mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-27 02:05:20 +02:00
Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd95311920 | |||
| 6da5c11731 | |||
| 4e58231308 | |||
| ef0ecf249a | |||
| 4981617f7a | |||
| 2070bc7007 | |||
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc | |||
| 84b42e9d19 | |||
| ed319a0e5f | |||
| dd55d10194 | |||
| 2084b46090 | |||
| 53443a6cf2 | |||
| 92715b5642 | |||
| 6166392515 | |||
| 49d0dead7d | |||
| 6f004830ff | |||
| e2e5e36bad | |||
| f267d264d3 | |||
| be1a77bfd7 | |||
| 41a980e826 | |||
| 09c09f3d64 | |||
| 2404399ec5 | |||
| b45d4c0557 | |||
| a41b138d3c | |||
| 1e46949dd6 | |||
| 3ed2c1ba5d | |||
| 809b99c9c9 | |||
| 4d3acdb5fb | |||
| ca9e321ef2 | |||
| da27517fcf | |||
| 192df0a3b8 | |||
| a965003a9d | |||
| 9ea26c821f | |||
| 14b699485a | |||
| 1684edc43f | |||
| 580c4418b9 | |||
| 4a65fc2358 | |||
| 71ba131fb3 | |||
| 9693b50719 | |||
| 102e2c54bb | |||
| e989590c08 | |||
| 6cee33b449 | |||
| f32498a444 | |||
| c85f71b601 | |||
| 196e55899e | |||
| ebec45076d | |||
| 561d9ae987 | |||
| 8950bd94cb | |||
| f416f197bc | |||
| 65afe5a0e6 | |||
| 4b5d347413 | |||
| 4dcc2dd0ca | |||
| 2a7a332160 | |||
| 27ee1eabda | |||
| 0034665965 | |||
| a69692be18 | |||
| dc76152166 | |||
| d7f3ae696c | |||
| 71f5449d34 | |||
| 0e64fa8d4c | |||
| 73b048d4c5 | |||
| 1c05b39861 | |||
| 7cfa6c163f | |||
| 2d4af2e867 | |||
| 1eeaffc442 | |||
| 82125b33ed | |||
| 42cbbc28fd | |||
| a7cbb0e93c | |||
| fde6148ece | |||
| df1661d75a | |||
| f938f79a35 | |||
| 333f00235b | |||
| c06475bfb3 | |||
| d1a54d0cf3 | |||
| 349437c06b | |||
| 1b03c83c84 | |||
| bb749aacf1 | |||
| 3a41b89e52 | |||
| 70cbc77381 | |||
| 3a99f5dfaa | |||
| f24435ecf4 | |||
| 4a708e316a | |||
| c2b47c998d | |||
| 534f7b3134 | |||
| d5d2692317 | |||
| dc9cc7b00f | |||
| 965e74c7e2 | |||
| 096ba54eb1 | |||
| f4e38f9e50 | |||
| c0d9409176 | |||
| 7d1f565749 | |||
| dfec4ada3b | |||
| cd695cf265 | |||
| 47ff2e0c38 | |||
| db7c09291f | |||
| 01f10c49ba | |||
| 1ff0692a72 | |||
| 116e6099d5 | |||
| 18ccaadc5b | |||
| 8f6eac7ca2 | |||
| f4610d0df5 | |||
| bf1a6b7d0a | |||
| b3fd05e62e | |||
| f7ce365618 | |||
| 77a558dbe5 | |||
| cc0c400b28 | |||
| 2bcd59cbfa | |||
| 5139acc7f1 | |||
| 1564433e02 | |||
| 1339beb7cd | |||
| cd9698ea48 | |||
| c8f8e4c5eb | |||
| 0b4ab46563 | |||
| ea1ac86134 | |||
| 790331e798 | |||
| f5d9b2ba41 | |||
| 7f26ac00b1 | |||
| fcbab10434 | |||
| c4061cc6ac | |||
| 12ac4d6b6f | |||
| 3d06e62cd4 | |||
| d7d23e1048 | |||
| 1fe9b70176 | |||
| a9cf8dd71a | |||
| 3299261db3 | |||
| e465ec8278 | |||
| d0e4a0aa1f | |||
| 74efec3235 | |||
| 13516087f2 | |||
| 0a0c16524a | |||
| 9b843a155e | |||
| cb085acbff | |||
| c3d7df166b | |||
| d312062125 | |||
| e2453192aa | |||
| 68eb0cc8f2 | |||
| cb9cecfa5d | |||
| 0f4e4a7d97 | |||
| f20a708b36 | |||
| 8c4e511883 | |||
| a4a3b8d664 | |||
| bf6530ea81 | |||
| 4a80c2aab1 | |||
| 527bbfe43f | |||
| d8e1edb60b | |||
| 245b5f74c0 | |||
| e9a1f63415 | |||
| ec370dd94b | |||
| e39d862ef3 | |||
| 7b065654aa | |||
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| 249e77a5d3 | |||
| 3cf4a52a69 | |||
| eb8b02756b | |||
| 0510d34ed3 | |||
| 1c8d12e72a | |||
| 0a36a6b674 | |||
| b887c9d50f | |||
| ee4e108e4f | |||
| 5e14a0fed4 | |||
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a | |||
| af6d219936 | |||
| 82a07e2e09 | |||
| 12a9b99fff | |||
| 3adf761158 | |||
| 670a4c61ff | |||
| 220f50d3bb | |||
| e0bf9d2a7c | |||
| f61cf46a52 | |||
| d188128d27 | |||
| f698c4120d | |||
| 338a852d49 | |||
| a64ee2242c | |||
| e9ff5e6f0b | |||
| f3911d8b68 | |||
| 9ce0be6450 | |||
| 6ab3eff61c | |||
| 0281da1c5a | |||
| 0b4770188c | |||
| 9376bb05fa | |||
| ecca3b6793 | |||
| f41a971cd8 | |||
| 44ba66d619 | |||
| bf685a607f | |||
| 5713cf0508 | |||
| bdd50d70ca | |||
| 8188399ce6 | |||
| f72b7dbbbb | |||
| 2409afcc5c | |||
| 15c0d02c13 | |||
| a54a5081e6 | |||
| db9dfcf049 | |||
| 47f9948748 | |||
| 05e866df55 | |||
| fc431f0cb8 | |||
| 228ab359ed | |||
| 103a8587f7 | |||
| 7db0083928 | |||
| e6f6ab499a | |||
| 721b7dbba0 | |||
| a95ddab814 | |||
| 2941546ae4 | |||
| bd9b9179c1 | |||
| ce7d54c151 | |||
| 3c778c07c2 | |||
| 95207341db | |||
| 70cf24924d | |||
| a8ebba691e | |||
| ec19ea44ad | |||
| ca8dc0f0f5 | |||
| 1dc50a697c | |||
| 1167c314ee | |||
| 55781e2b34 | |||
| 7439e44e44 | |||
| cf2639df3d | |||
| 834de928c2 | |||
| 72efb21439 | |||
| aa8790ebdb | |||
| 6d491052ee | |||
| 87ff4691ce | |||
| 34d76e79ed | |||
| 31b43da96f | |||
| 0540e673e2 | |||
| 4e88a63809 | |||
| f7581f8a65 | |||
| e87a1c079c | |||
| 3f9477c246 | |||
| 05ed1e188e | |||
| f3d06e49f8 | |||
| f9a4b68967 | |||
| 3631cfe365 | |||
| da6eef905c | |||
| 8766ae176e | |||
| 36b53d490f | |||
| f9b8b812a4 | |||
| ac9eae5272 | |||
| f270cc00d8 | |||
| a5a3f970da | |||
| 987c465bf8 | |||
| cf3c766fd9 | |||
| 7efafae432 | |||
| 1b8f44dde3 | |||
| 4d93246863 | |||
| 0471886d9f | |||
| 266974b799 | |||
| c3663c67d7 | |||
| 07bb23d10b | |||
| 749fc22c6b | |||
| 9f9a4e8298 | |||
| 39e7d64d3f | |||
| 35d8610c00 | |||
| bc550ae8f5 | |||
| c76ef7f19b | |||
| b7781264d3 | |||
| 696e03941a | |||
| 4609a351dc | |||
| c275415a49 | |||
| 486ebd6bc8 | |||
| 74b9926647 | |||
| 2a6ba6d541 | |||
| 931216ab7d | |||
| 916936e179 | |||
| b535353365 | |||
| be2ae096ee | |||
| 948b85ddcb | |||
| ae904b4cd8 | |||
| aad50e7b50 | |||
| ff28a07871 |
@@ -0,0 +1,80 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
|
labels: ["bug", "new"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Thank you for taking the time to fill out this bug report.
|
||||||
|
|
||||||
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
|
## Filing a bug report
|
||||||
|
|
||||||
|
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
||||||
|
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
||||||
|
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: grayjay-version
|
||||||
|
attributes:
|
||||||
|
label: Grayjay Version
|
||||||
|
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
|
||||||
|
placeholder: "242"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: plugin
|
||||||
|
attributes:
|
||||||
|
label: What plugins are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- All
|
||||||
|
- Youtube
|
||||||
|
- BiliBili (CN)
|
||||||
|
- Twitch
|
||||||
|
- Odysee
|
||||||
|
- Rumble
|
||||||
|
- Kick
|
||||||
|
- PeerTube
|
||||||
|
- Patreon
|
||||||
|
- Nebula
|
||||||
|
- SoundCloud
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: plugin-version
|
||||||
|
attributes:
|
||||||
|
label: Plugin Version
|
||||||
|
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
|
||||||
|
placeholder: "12"
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: login
|
||||||
|
attributes:
|
||||||
|
label: When do you experience the issue?
|
||||||
|
options:
|
||||||
|
- label: While logged in
|
||||||
|
- label: While logged out
|
||||||
|
- label: N/A
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Need a Grayjay License?
|
||||||
|
url: https://pay.futo.org/api/PaymentPortal
|
||||||
|
about: Purchase a Grayjay license with FutoPay
|
||||||
|
- name: Plugin Building, Usage, or other Questions
|
||||||
|
url: https://chat.futo.org/#narrow/stream/46-Grayjay
|
||||||
|
about: Grayjays Community Chat
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: Documentation Issue
|
||||||
|
description: Report an issue or suggest a change in the documentation.
|
||||||
|
labels: ["documentation", "new"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Thank you for opening a documentation change request.
|
||||||
|
|
||||||
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||||
|
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)
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-affected-pages
|
||||||
|
attributes:
|
||||||
|
label: Affected Pages
|
||||||
|
description: |
|
||||||
|
Link to or describe the pages relevant to your documentation change request.
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-problem
|
||||||
|
attributes:
|
||||||
|
label: What is the docs issue?
|
||||||
|
description: What problems or suggestions do you have about the documentation?
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposal
|
||||||
|
description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content.
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-references
|
||||||
|
attributes:
|
||||||
|
label: References
|
||||||
|
description: |
|
||||||
|
Are there any other open or closed GitLab/GitHub issues related to the problem or solution you described? If so, list them below. For example:
|
||||||
|
```
|
||||||
|
- #6017
|
||||||
|
```
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or other enhancement.
|
||||||
|
labels: ["enhancement", "new"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Thank you for opening a feature request.
|
||||||
|
|
||||||
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues 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)
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-use-case
|
||||||
|
attributes:
|
||||||
|
label: Use Cases
|
||||||
|
description: |
|
||||||
|
In order to properly evaluate a feature request, it is necessary to understand the use cases for it. Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. Please keep this section focused on the problem and not on the suggested solution.
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposal
|
||||||
|
description: |
|
||||||
|
If you have an idea for a way to address the problem via a change to Grayjay features, please describe it below.
|
||||||
|
|
||||||
|
In this section, it's helpful to include specific examples of how what you are suggesting might look in the application, this allows us to understand the full picture of what you are proposing. If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Grayjay Core Application.
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: grayjay-references
|
||||||
|
attributes:
|
||||||
|
label: References
|
||||||
|
description: |
|
||||||
|
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above or to the suggested solution? If so, please create a list below that mentions each of them. For example:
|
||||||
|
```
|
||||||
|
- #10
|
||||||
|
```
|
||||||
|
placeholder:
|
||||||
|
value:
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||||
|
|
||||||
@@ -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 }}
|
||||||
+12
@@ -70,3 +70,15 @@
|
|||||||
[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
|
||||||
|
|||||||
+16
-2
@@ -49,9 +49,23 @@ 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.**
|
|
||||||
|
|
||||||
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.
|
### License
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# Grayjay Core License 1.0
|
# Source First License 1.1
|
||||||
|
|
||||||
## 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 Licensor’s 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 Licensor’s 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 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.
|
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.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -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.jpg" height="700" /></b></td>
|
<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-details.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Video</td>
|
<td>Video</td>
|
||||||
@@ -24,12 +24,10 @@ 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/sources.jpg" height="700" /></b></td>
|
<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-disabled.jpg" height="700" /></b></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Sources (all enabled)</td>
|
<td>Sources</td>
|
||||||
<td>Sources (one disabled)</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -38,7 +36,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Install a new source</td>
|
<td>Install a new source</td>
|
||||||
@@ -54,8 +52,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.jpg" height="700" /></b></td>
|
<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-preview.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Search (list)</td>
|
<td>Search (list)</td>
|
||||||
@@ -71,7 +69,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Channel</td>
|
<td>Channel</td>
|
||||||
@@ -112,7 +110,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Settings</td>
|
<td>Settings</td>
|
||||||
@@ -125,8 +123,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Playlists</td>
|
<td>Playlists</td>
|
||||||
@@ -142,7 +140,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Downloads</td>
|
<td>Downloads</td>
|
||||||
@@ -157,7 +155,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.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Casting</td>
|
<td>Casting</td>
|
||||||
@@ -182,6 +180,12 @@ 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.
|
||||||
|
|
||||||
@@ -199,7 +203,6 @@ 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).
|
||||||
|
|||||||
+11
-2
@@ -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 '1.7.2'
|
id 'org.ajoberstar.grgit' version '5.2.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,9 +144,19 @@ 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'
|
||||||
@@ -184,7 +194,6 @@ 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'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<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"/>
|
||||||
@@ -35,15 +36,18 @@
|
|||||||
<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" />
|
||||||
<service android:name=".services.DownloadService"
|
<service android:name=".services.DownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
<service android:name=".services.ExportingService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
|
|
||||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||||
@@ -53,9 +57,8 @@
|
|||||||
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="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
@@ -148,34 +151,31 @@
|
|||||||
<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:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
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>
|
||||||
@@ -189,44 +189,55 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
|
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>
|
||||||
@@ -201,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; //Thumbnail[]
|
this.thumbnails = obj.thumbnails ?? new 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
|
||||||
@@ -278,12 +278,49 @@ class PlatformPostDetails extends PlatformPost {
|
|||||||
super(obj);
|
super(obj);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformPostDetails";
|
this.plugin_type = "PlatformPostDetails";
|
||||||
this.rating = obj.rating ?? RatingLikes(-1);
|
this.rating = obj.rating ?? new 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) {
|
||||||
@@ -330,6 +367,16 @@ 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);
|
||||||
@@ -362,8 +409,26 @@ 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 {
|
||||||
@@ -406,6 +471,49 @@ 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) {
|
||||||
@@ -436,7 +544,7 @@ class PlatformPlaylist extends PlatformContent {
|
|||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 4);
|
super(obj, 4);
|
||||||
this.plugin_type = "PlatformPlaylist";
|
this.plugin_type = "PlatformPlaylist";
|
||||||
this.videoCount = obj.videoCount ?? 0;
|
this.videoCount = obj.videoCount ?? -1;
|
||||||
this.thumbnail = obj.thumbnail;
|
this.thumbnail = obj.thumbnail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,3 +870,99 @@ 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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ 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();
|
||||||
return if (requestModifier != null) {
|
val requestExecutor = getRequestExecutor();
|
||||||
|
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
@@ -1,6 +1,9 @@
|
|||||||
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
|
||||||
@@ -25,4 +28,18 @@ 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 -> {
|
||||||
|
"[${toString()}]"
|
||||||
|
}
|
||||||
|
is Inet4Address -> {
|
||||||
|
toString()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw Exception("Invalid address type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ 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
|
||||||
@@ -32,7 +33,6 @@ 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,6 +44,7 @@ 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);
|
||||||
|
|
||||||
@@ -57,7 +58,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||||
|
@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 {
|
||||||
@@ -73,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
||||||
@FormFieldButton(R.drawable.ic_quiz)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +93,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
||||||
@FormFieldButton(R.drawable.ic_data_alert)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +125,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
@@ -129,16 +139,15 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||||
@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, -1)
|
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
@@ -148,6 +157,24 @@ 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
|
||||||
@@ -326,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
@@ -353,7 +380,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
//= 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, 1)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
||||||
@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) {
|
||||||
@@ -369,37 +396,29 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
||||||
@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, 3)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
||||||
@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, 4)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
||||||
@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.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
var simplifySources: Boolean = true;
|
||||||
var autoRotate: Int = 2;
|
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
@FormField(R.string.force_allow_full_screen_rotation, FieldForm.TOGGLE, R.string.force_allow_full_screen_rotation_description, 5)
|
||||||
|
var forceAllowFullScreenRotation: Boolean = false
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 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;
|
||||||
|
|
||||||
@@ -450,18 +469,44 @@ 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 = 0;
|
var defaultCommentSection: Int = 2;
|
||||||
|
|
||||||
|
@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)
|
||||||
@@ -510,7 +555,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 = true;
|
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||||
@@ -525,6 +570,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, 1)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var alwaysProxyRequests: 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)
|
||||||
@@ -775,10 +824,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", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
||||||
StateBackup.saveExternalBackup(activity);
|
StateBackup.saveExternalBackup(activity);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -794,10 +843,14 @@ 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() {
|
||||||
StatePayment.instance.clearLicenses();
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
SettingsActivity.getActivity()?.let {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
StatePayment.instance.clearLicenses();
|
||||||
it.reloadSettings();
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
|
it.reloadSettings();
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -806,12 +859,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var other = Other();
|
var other = Other();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
var bypassRotationPrevention: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
||||||
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
@@ -843,7 +898,24 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var pan: Boolean = true;
|
var pan: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
|
@FormField(R.string.synchronization, 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.info, FieldForm.GROUP, -1, 21)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -234,13 +235,17 @@ 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()!!;
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
try {
|
||||||
|
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)
|
||||||
@@ -491,6 +496,13 @@ 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
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
|
||||||
@@ -197,7 +199,6 @@ 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) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
val builder = AlertDialog.Builder(context);
|
val 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);
|
||||||
@@ -213,28 +214,31 @@ class UIDialogs {
|
|||||||
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;
|
|
||||||
else
|
|
||||||
this.text = textDetails;
|
|
||||||
};
|
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
|
||||||
if(code == null)
|
|
||||||
this.visibility = View.GONE;
|
this.visibility = View.GONE;
|
||||||
|
else {
|
||||||
|
this.text = textDetails;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
|
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 {
|
||||||
if(actions.size > 1)
|
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||||
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||||
};
|
};
|
||||||
buttonView.setTextColor(Color.WHITE);
|
buttonView.setTextColor(Color.WHITE);
|
||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
@@ -256,7 +260,7 @@ class UIDialogs {
|
|||||||
|
|
||||||
return@map buttonView;
|
return@map buttonView;
|
||||||
};
|
};
|
||||||
if(actions.size <= 1)
|
if(actions.size <= 1 || center)
|
||||||
this.gravity = Gravity.CENTER;
|
this.gravity = Gravity.CENTER;
|
||||||
else
|
else
|
||||||
this.gravity = Gravity.END;
|
this.gravity = Gravity.END;
|
||||||
@@ -345,6 +349,13 @@ 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);
|
||||||
@@ -507,11 +518,13 @@ 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) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -15,14 +15,19 @@ 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
|
||||||
@@ -34,12 +39,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
|
||||||
@@ -91,9 +96,17 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
container.context,
|
||||||
}, false),
|
R.drawable.ic_notifications,
|
||||||
|
"Notifications",
|
||||||
|
"",
|
||||||
|
tag = "notifications",
|
||||||
|
call = {
|
||||||
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
),
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
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.",
|
||||||
@@ -128,22 +141,62 @@ 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(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
container.context,
|
||||||
}, false) else null,
|
R.drawable.ic_live_tv,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
"Livestreams",
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
"Check for livestreams",
|
||||||
}, false) else null,
|
tag = "fetchLive",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
"Streams",
|
||||||
|
"Check for streams",
|
||||||
|
tag = "fetchStreams",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
SlideUpMenuItem(
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
container.context,
|
||||||
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
R.drawable.ic_play,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
"Videos",
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
"Check for videos",
|
||||||
}, false) else null,
|
tag = "fetchVideos",
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
call = {
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
}, 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",
|
||||||
@@ -242,11 +295,23 @@ 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, {
|
|
||||||
selectedAudioVariant = it
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
audioButtons.add(SlideUpMenuItem(
|
||||||
}, false))
|
container.context,
|
||||||
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedAudioVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
@@ -258,11 +323,22 @@ class UISlideOverlays {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
masterPlaylist.getVideoSources().forEach {
|
masterPlaylist.getVideoSources().forEach {
|
||||||
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideoVariant = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
videoButtons.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
container.context,
|
||||||
}, false))
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedVideoVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
@@ -321,8 +397,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
|
|
||||||
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
||||||
var selectedVideo: IVideoUrlSource? = null;
|
var selectedVideo: IVideoSource? = null;
|
||||||
var selectedAudio: IAudioUrlSource? = null;
|
var selectedAudio: IAudioSource? = null;
|
||||||
var selectedSubtitle: ISubtitleSource? = null;
|
var selectedSubtitle: ISubtitleSource? = null;
|
||||||
|
|
||||||
val videoSources = descriptor.videoSources;
|
val videoSources = descriptor.videoSources;
|
||||||
@@ -341,45 +417,93 @@ 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(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
listOf(listOf(SlideUpMenuItem(
|
||||||
selectedVideo = null;
|
container.context,
|
||||||
menu?.selectOption(videoSources, "none");
|
R.drawable.ic_movie,
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context.getString(R.string.none),
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.audio_only),
|
||||||
}, false)) +
|
tag = "none",
|
||||||
|
call = {
|
||||||
|
selectedVideo = null;
|
||||||
|
menu?.selectOption(videoSources, "none");
|
||||||
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IVideoUrlSource -> {
|
is IVideoUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideo = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(videoSources, it);
|
SlideUpMenuItem(
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context,
|
||||||
menu?.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 = {
|
||||||
|
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(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).flatten().toList()
|
}.filterNotNull()).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 IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
) as IVideoUrlSource?;
|
) as IVideoSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSources != null) {
|
if (audioSources != null) {
|
||||||
@@ -388,43 +512,90 @@ class UISlideOverlays {
|
|||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IAudioUrlSource -> {
|
is IAudioUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedAudio = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(audioSources, it);
|
SlideUpMenuItem(
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context,
|
||||||
}, false);
|
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 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(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS Audio",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}.filterNotNull()));
|
||||||
|
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && 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 IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(container.context, R.drawable.ic_edit, it.name, "", it, {
|
SlideUpMenuItem(
|
||||||
if (selectedSubtitle == it) {
|
container.context,
|
||||||
selectedSubtitle = null;
|
R.drawable.ic_edit,
|
||||||
menu?.selectOption(subtitleSources, null);
|
it.name,
|
||||||
} else {
|
"",
|
||||||
selectedSubtitle = it;
|
tag = it,
|
||||||
menu?.selectOption(subtitleSources, it);
|
call = {
|
||||||
}
|
if (selectedSubtitle == it) {
|
||||||
}, false);
|
selectedSubtitle = null;
|
||||||
|
menu?.selectOption(subtitleSources, null);
|
||||||
|
} else {
|
||||||
|
selectedSubtitle = it;
|
||||||
|
menu?.selectOption(subtitleSources, it);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -442,6 +613,18 @@ 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) {
|
||||||
@@ -498,8 +681,9 @@ 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));
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
@@ -536,23 +720,47 @@ 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(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
SlideUpMenuItem(
|
||||||
targetPxSize = it.third;
|
container.context,
|
||||||
menu?.selectOption("Video", it.third);
|
R.drawable.ic_movie,
|
||||||
}, false)
|
it.first,
|
||||||
|
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(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
SlideUpMenuItem(
|
||||||
targetBitrate = 1;
|
container.context,
|
||||||
menu?.selectOption("Bitrate", 1);
|
R.drawable.ic_movie,
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.low_bitrate),
|
||||||
}, false),
|
"",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
tag = 1,
|
||||||
targetBitrate = 9999999;
|
call = {
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
targetBitrate = 1;
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.selectOption("Bitrate", 1);
|
||||||
}, false)
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
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
|
||||||
|
)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
|
||||||
@@ -672,11 +880,21 @@ 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, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -688,42 +906,91 @@ 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(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
if(!isLimited)
|
||||||
showDownloadVideoOverlay(video, container, true);
|
SlideUpMenuItem(
|
||||||
}, false),
|
container.context,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
R.drawable.ic_download,
|
||||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
container.context.getString(R.string.download),
|
||||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
container.context.getString(R.string.download_the_video),
|
||||||
action = Intent.ACTION_SEND;
|
tag = "download",
|
||||||
putExtra(Intent.EXTRA_TEXT, url);
|
call = {
|
||||||
type = "text/plain";
|
showDownloadVideoOverlay(video, container, true);
|
||||||
}, null));
|
},
|
||||||
}, false),
|
invokeParent = false
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
) else null,
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
SlideUpMenuItem(
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
container.context,
|
||||||
}))
|
R.drawable.ic_share,
|
||||||
+ actions)
|
container.context.getString(R.string.share),
|
||||||
|
"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, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
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",
|
container.context.getString(R.string.add_to_queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
|
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(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", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
};
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
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, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
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),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -745,8 +1012,12 @@ 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, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -758,25 +1029,44 @@ 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, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
container.context.getString(R.string.queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
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),
|
tag = "queue",
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
|
SlideUpMenuItem(container.context,
|
||||||
|
R.drawable.ic_watchlist_add,
|
||||||
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "watch later",
|
||||||
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<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", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
});
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
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, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
playlistItems.add(SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
playlist.name,
|
||||||
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -801,20 +1091,36 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
val views = arrayOf(
|
val views = arrayOf(
|
||||||
hidden
|
hidden
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
.map { btn -> SlideUpMenuItem(
|
||||||
btn.handler?.invoke(btn);
|
container.context,
|
||||||
}, invokeParents) as View }.toTypedArray(),
|
btn.iconResource,
|
||||||
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), "", {
|
btn.text.text.toString(),
|
||||||
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
|
tag = "",
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
call = {
|
||||||
.filter { it != null }
|
btn.handler?.invoke(btn);
|
||||||
.map { it!! }
|
},
|
||||||
.toList();
|
invokeParent = invokeParents
|
||||||
|
) 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() };
|
||||||
@@ -826,14 +1132,21 @@ class UISlideOverlays {
|
|||||||
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,
|
||||||
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
options.map { SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_move_up,
|
||||||
|
it.first,
|
||||||
|
"",
|
||||||
|
tag = it.second,
|
||||||
|
call = {
|
||||||
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);
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
selection.remove(it.second);
|
selection.remove(it.second);
|
||||||
}, false)
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
});
|
});
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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
|
||||||
@@ -25,10 +26,13 @@ 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.File
|
||||||
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.nio.ByteOrder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
|
||||||
@@ -229,4 +233,49 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+39
-21
@@ -3,6 +3,7 @@ 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
|
||||||
@@ -17,6 +18,7 @@ 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.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,6 +29,7 @@ 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;
|
||||||
@@ -43,6 +46,7 @@ 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();
|
||||||
};
|
};
|
||||||
@@ -65,35 +69,49 @@ 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 {
|
||||||
processHandle = ProcessHandle.create();
|
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
processHandle = ProcessHandle.create();
|
||||||
|
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(PolycentricCache.SERVER);
|
||||||
|
processHandle.setUsername(username);
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||||
|
return@launch;
|
||||||
|
} finally {
|
||||||
|
_creating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(PolycentricCache.SERVER);
|
try {
|
||||||
processHandle.setUsername(username);
|
Logger.i(TAG, "Started backfill");
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
} catch (e: Throwable) {
|
Logger.i(TAG, "Finished backfill");
|
||||||
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
} catch (e: Throwable) {
|
||||||
return@launch;
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
} finally {
|
}
|
||||||
_creating = false;
|
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
try {
|
withContext(Dispatchers.Main) {
|
||||||
Logger.i(TAG, "Started backfill");
|
_profileName.isEnabled = true;
|
||||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
_buttonCreate.visibility = View.VISIBLE;
|
||||||
Logger.i(TAG, "Finished backfill");
|
_loader.stop();
|
||||||
} catch (e: Throwable) {
|
_loader.visibility = View.GONE;
|
||||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -28,6 +29,7 @@ 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))
|
||||||
@@ -42,6 +44,7 @@ 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();
|
||||||
};
|
};
|
||||||
@@ -78,6 +81,7 @@ 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));
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
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
|
||||||
@@ -33,6 +34,7 @@ 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.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
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
|
||||||
@@ -47,6 +49,7 @@ 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;
|
||||||
@@ -68,10 +71,14 @@ 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();
|
||||||
@@ -92,6 +99,16 @@ 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));
|
||||||
@@ -108,6 +125,7 @@ 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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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
|
||||||
@@ -184,12 +185,19 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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(if (connected) LinkType.Local else LinkType.None)
|
||||||
|
.setName(publicKey)
|
||||||
|
.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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) { session, complete, message ->
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (complete) {
|
||||||
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
_textPairingStatus.text = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
|
_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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,14 @@ 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 val _builderTemplate: OkHttpClient.Builder;
|
protected var _builderTemplate: OkHttpClient.Builder;
|
||||||
|
|
||||||
private var client: OkHttpClient;
|
private var client: OkHttpClient;
|
||||||
|
|
||||||
@@ -32,6 +33,15 @@ 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?) { }
|
||||||
@@ -62,6 +72,15 @@ 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;
|
||||||
|
|||||||
@@ -210,6 +210,20 @@ 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");
|
||||||
|
|
||||||
|
|||||||
@@ -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.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
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(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("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(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("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(HttpFuntionHandler("GET", getField.second.path) {
|
addHandler(HttpFunctionHandler("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(
|
||||||
|
|||||||
+1
-1
@@ -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 HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
class HttpFunctionHandler(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);
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media
|
|
||||||
|
|
||||||
import androidx.collection.LruCache
|
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A temporary class that caches video results
|
|
||||||
* In future this should be part of a bigger system
|
|
||||||
*/
|
|
||||||
class CachedPlatformClient : IPlatformClient {
|
|
||||||
private val _client : IPlatformClient;
|
|
||||||
override val id: String get() = _client.id;
|
|
||||||
override val name: String get() = _client.name;
|
|
||||||
override val icon: ImageVariable? get() = _client.icon;
|
|
||||||
|
|
||||||
private val _cache: LruCache<String, IPlatformContentDetails>;
|
|
||||||
|
|
||||||
override val capabilities: PlatformClientCapabilities
|
|
||||||
get() = _client.capabilities;
|
|
||||||
|
|
||||||
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
|
|
||||||
this._client = client;
|
|
||||||
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
|
|
||||||
}
|
|
||||||
override fun initialize() { _client.initialize() }
|
|
||||||
override fun disable() { _client.disable() }
|
|
||||||
|
|
||||||
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
|
|
||||||
override fun getContentDetails(url: String): IPlatformContentDetails {
|
|
||||||
var result = _cache.get(url);
|
|
||||||
if(result == null) {
|
|
||||||
result = _client.getContentDetails(url);
|
|
||||||
_cache.put(url, result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
|
||||||
|
|
||||||
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
|
||||||
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
|
|
||||||
|
|
||||||
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
|
|
||||||
override fun getChannelContents(
|
|
||||||
channelUrl: String,
|
|
||||||
type: String?,
|
|
||||||
order: String?,
|
|
||||||
filters: Map<String, List<String>>?
|
|
||||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
|
||||||
|
|
||||||
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = _client.getChannelPlaylists(channelUrl);
|
|
||||||
|
|
||||||
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
|
||||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
|
||||||
|
|
||||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
|
||||||
|
|
||||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
|
||||||
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
|
|
||||||
override fun search(
|
|
||||||
query: String,
|
|
||||||
type: String?,
|
|
||||||
order: String?,
|
|
||||||
filters: Map<String, List<String>>?
|
|
||||||
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
|
|
||||||
|
|
||||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
|
|
||||||
override fun searchChannelContents(
|
|
||||||
channelUrl: String,
|
|
||||||
query: String,
|
|
||||||
type: String?,
|
|
||||||
order: String?,
|
|
||||||
filters: Map<String, List<String>>?
|
|
||||||
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
|
|
||||||
|
|
||||||
override fun searchChannels(query: String) = _client.searchChannels(query);
|
|
||||||
|
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
|
|
||||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
|
|
||||||
|
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
|
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
|
|
||||||
|
|
||||||
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
|
|
||||||
|
|
||||||
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
|
|
||||||
|
|
||||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
|
|
||||||
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
|
|
||||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
|
|
||||||
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
|
|
||||||
|
|
||||||
override fun isClaimTypeSupported(claimType: Int): Boolean {
|
|
||||||
return _client.isClaimTypeSupported(claimType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -121,6 +121,11 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content recommendations
|
||||||
|
*/
|
||||||
|
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
|
||||||
|
|
||||||
|
|
||||||
//Comments
|
//Comments
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetLiveChatWindow: Boolean = false,
|
val hasGetLiveChatWindow: Boolean = false,
|
||||||
val hasGetContentChapters: Boolean = false,
|
val hasGetContentChapters: Boolean = false,
|
||||||
val hasPeekChannelContents: Boolean = false,
|
val hasPeekChannelContents: Boolean = false,
|
||||||
val hasGetChannelPlaylists: Boolean = false
|
val hasGetChannelPlaylists: Boolean = false,
|
||||||
|
val hasGetContentRecommendations: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,15 @@ 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) {
|
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
||||||
_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");
|
||||||
@@ -51,7 +53,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();
|
reserved = _parent.getCopy(_privatePool);
|
||||||
|
|
||||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
StateApp.instance.handleCaptchaException(client, ex);
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ 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) {
|
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
||||||
_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 {
|
||||||
@@ -19,7 +21,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).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
||||||
this.onDead.subscribe { _, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class Serializer {
|
class Serializer {
|
||||||
companion object {
|
companion object {
|
||||||
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
|
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@ 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);
|
||||||
|
|||||||
@@ -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.values().firstOrNull { it.value == value };
|
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,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.values().firstOrNull { it.value == value };
|
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+2
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
|
|||||||
|
|
||||||
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
||||||
fun getPlaybackTracker(): IPlaybackTracker?;
|
fun getPlaybackTracker(): IPlaybackTracker?;
|
||||||
|
|
||||||
|
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
|
||||||
}
|
}
|
||||||
@@ -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.values().first { it.value == value };
|
return LiveEventType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
|
|||||||
//TODO: Determine if this should be IPlatformContent (probably not?)
|
//TODO: Determine if this should be IPlatformContent (probably not?)
|
||||||
val contents: IPager<IPlatformVideo>;
|
val contents: IPager<IPlatformVideo>;
|
||||||
|
|
||||||
fun toPlaylist(): Playlist;
|
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
|
||||||
}
|
}
|
||||||
@@ -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.values().firstOrNull { it.value == value };
|
val result = TextType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -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.values().first { it.value == value };
|
return RatingType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -1,6 +1,3 @@
|
|||||||
package com.futo.platformplayer.api.media.models.streams.sources
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
||||||
val bearerToken: String
|
|
||||||
val licenseUri: String
|
|
||||||
}
|
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IDashManifestWidevineSource : IWidevineSource {
|
||||||
|
val url: String
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
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?
|
||||||
|
}
|
||||||
+2
-2
@@ -19,9 +19,9 @@ open class SerializedPlatformVideo(
|
|||||||
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)
|
||||||
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,
|
||||||
|
|||||||
+2
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
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.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.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
@@ -56,6 +57,7 @@ open class SerializedPlatformVideoDetails(
|
|||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
||||||
|
|||||||
@@ -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(): JSClient {
|
override fun getCopy(privateCopy: Boolean): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -164,13 +164,16 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||||
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;
|
||||||
_auth = descriptor.getAuth();
|
if(!withoutCredentials)
|
||||||
|
_auth = descriptor.getAuth();
|
||||||
|
else
|
||||||
|
_auth = null;
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
@@ -190,8 +193,8 @@ open class JSClient : IPlatformClient {
|
|||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
||||||
return JSClient(_context, descriptor, saveState(), _script);
|
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
@@ -234,7 +237,8 @@ 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 {
|
||||||
@@ -560,7 +564,7 @@ open class JSClient : IPlatformClient {
|
|||||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
@JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||||
if(!capabilities.hasGetLiveChatWindow)
|
if(!capabilities.hasGetLiveChatWindow)
|
||||||
@@ -569,7 +573,7 @@ open class JSClient : IPlatformClient {
|
|||||||
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
||||||
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
@JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
@@ -578,6 +582,20 @@ open class JSClient : IPlatformClient {
|
|||||||
return@isBusyWith JSLiveEventPager(config, this,
|
return@isBusyWith JSLiveEventPager(config, this,
|
||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
||||||
|
@JSDocsParameter("url", "Url of content")
|
||||||
|
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
||||||
|
if(!capabilities.hasGetContentRecommendations)
|
||||||
|
return@isBusyWith null;
|
||||||
|
ensureEnabled();
|
||||||
|
return@isBusyWith JSContentPager(config, this,
|
||||||
|
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||||
@JSDocsParameter("query", "Query that search results should match")
|
@JSDocsParameter("query", "Query that search results should match")
|
||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
class JSClientConstants {
|
||||||
|
companion object {
|
||||||
|
val PLUGIN_SPEC_VERSION = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -4,7 +4,9 @@ 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
|
||||||
|
|
||||||
@@ -48,6 +50,8 @@ 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,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -77,7 +81,8 @@ 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!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,7 +175,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 };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+1
-1
@@ -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 && StateDeveloper.instance.devProxy != null)
|
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && 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)
|
||||||
))
|
))
|
||||||
|
|||||||
+1
@@ -16,6 +16,7 @@ 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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -42,7 +42,12 @@ 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);
|
||||||
|
if(authorObj != null)
|
||||||
|
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||||
|
else
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
if(datetimeInt == 0.toLong())
|
if(datetimeInt == 0.toLong())
|
||||||
@@ -54,4 +59,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
_hasGetDetails = _content.has("getDetails");
|
_hasGetDetails = _content.has("getDetails");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUnderlyingObject(): V8ValueObject? {
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,8 @@ 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();
|
||||||
|
|||||||
+1
-1
@@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
|
|||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||||
val contextName = "Playlist";
|
val contextName = "Playlist";
|
||||||
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
||||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!;
|
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
-8
@@ -7,32 +7,37 @@ 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.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
|
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>;
|
||||||
|
|
||||||
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
|
contents = ReusablePager(JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")));
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toPlaylist(): Playlist {
|
override fun toPlaylist(onProgress: ((progress: Int) -> Unit)?): Playlist {
|
||||||
val videos = contents.getResults().toMutableList();
|
val playlist = if (contents is ReusablePager) contents.getWindow() else contents;
|
||||||
|
val videos = playlist.getResults().toMutableList();
|
||||||
|
onProgress?.invoke(videos.size);
|
||||||
|
|
||||||
//Download all pages
|
//Download all pages
|
||||||
var allowedEmptyCount = 2;
|
var allowedEmptyCount = 2;
|
||||||
while(contents.hasMorePages()) {
|
while(playlist.hasMorePages()) {
|
||||||
contents.nextPage();
|
playlist.nextPage();
|
||||||
if(!videos.addAll(contents.getResults())) {
|
if(!videos.addAll(playlist.getResults())) {
|
||||||
allowedEmptyCount--;
|
allowedEmptyCount--;
|
||||||
if(allowedEmptyCount <= 0)
|
if(allowedEmptyCount <= 0)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else allowedEmptyCount = 2;
|
else allowedEmptyCount = 2;
|
||||||
|
|
||||||
|
onProgress?.invoke(videos.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
@@ -3,6 +3,7 @@ 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.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||||
@@ -18,6 +19,7 @@ import com.futo.platformplayer.states.StateDeveloper
|
|||||||
|
|
||||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||||
private val _hasGetComments: Boolean;
|
private val _hasGetComments: Boolean;
|
||||||
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
|
|
||||||
override val rating: IRating;
|
override val rating: IRating;
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
|||||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||||
|
|
||||||
_hasGetComments = _content.has("getComments");
|
_hasGetComments = _content.has("getComments");
|
||||||
|
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
@@ -51,9 +54,27 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
|||||||
}
|
}
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = 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 {
|
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
+21
@@ -6,6 +6,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
|||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
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.IRating
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
@@ -27,6 +28,7 @@ import com.futo.platformplayer.states.StateDeveloper
|
|||||||
|
|
||||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||||
private val _hasGetComments: Boolean;
|
private val _hasGetComments: Boolean;
|
||||||
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
private val _hasGetPlaybackTracker: Boolean;
|
private val _hasGetPlaybackTracker: Boolean;
|
||||||
|
|
||||||
//Details
|
//Details
|
||||||
@@ -66,6 +68,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
|
|
||||||
_hasGetComments = _content.has("getComments");
|
_hasGetComments = _content.has("getComments");
|
||||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||||
|
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
@@ -89,6 +92,24 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||||
|
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if(client is DevJSClient)
|
||||||
|
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.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);
|
||||||
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
if(client !is JSClient || !_hasGetComments || _content.isClosed)
|
if(client !is JSClient || !_hasGetComments || _content.isClosed)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+20
-3
@@ -3,22 +3,39 @@ 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, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -35,4 +35,9 @@ 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
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.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 {
|
||||||
|
override val container : String = "application/dash+xml";
|
||||||
|
override val name : String;
|
||||||
|
override val codec: String;
|
||||||
|
override val bitrate: Int;
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean;
|
||||||
|
|
||||||
|
override val language: String;
|
||||||
|
|
||||||
|
val url: String;
|
||||||
|
override var manifest: String?;
|
||||||
|
|
||||||
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
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();
|
||||||
|
if(_plugin is DevJSClient)
|
||||||
|
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
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.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 {
|
||||||
|
override val container : String = "application/dash+xml";
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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");
|
||||||
|
if(_plugin is DevJSClient) {
|
||||||
|
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
|
if(audioAdaptationSet != null) {
|
||||||
|
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return videoDash;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-8
@@ -10,10 +10,12 @@ 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
|
||||||
|
|
||||||
@@ -21,9 +23,17 @@ 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) {
|
||||||
@@ -36,6 +46,11 @@ 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? {
|
||||||
@@ -44,20 +59,38 @@ 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";
|
||||||
@@ -65,33 +98,49 @@ 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);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||||
|
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 -> throw NotImplementedError("Unknown type ${type}");
|
else -> {
|
||||||
|
Logger.w("JSSource", "Unknown audio type ${type}");
|
||||||
|
null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -23,9 +23,11 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -21,6 +21,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -33,4 +33,9 @@ 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,8 @@ class DashBuilder : XMLBuilder {
|
|||||||
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
||||||
withRepresentation(id, mapOf(
|
withRepresentation(id, mapOf(
|
||||||
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
||||||
Pair("startWithSAP", "1"),
|
Pair("default", "true"),
|
||||||
|
Pair("lang", "en"),
|
||||||
Pair("bandwidth", "1000")
|
Pair("bandwidth", "1000")
|
||||||
)) {
|
)) {
|
||||||
it.withBaseURL(subtitleUrl)
|
it.withBaseURL(subtitleUrl)
|
||||||
@@ -151,7 +152,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
//TODO: Verify if & really should be replaced like this?
|
//TODO: Verify if & really should be replaced like this?
|
||||||
it.withRepresentationOnDemand("1", subtitleSource, subtitleUrl.replace("&", "&"))
|
it.withRepresentationOnDemand("caption_en", subtitleSource, subtitleUrl.replace("&", "&"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Video
|
//Video
|
||||||
@@ -164,7 +165,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
Pair("subsegmentStartsWithSAP", "1")
|
Pair("subsegmentStartsWithSAP", "1")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
it.withRepresentationOnDemand("1", vidSource, vidUrl.replace("&", "&"));
|
it.withRepresentationOnDemand("2", vidSource, vidUrl.replace("&", "&"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import android.net.Uri
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.BuildConfig
|
import android.util.Xml
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
@@ -25,16 +29,23 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSou
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
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.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.mdns.DnsService
|
||||||
|
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
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.stores.CastingDeviceInfoStorage
|
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.toUrlAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -42,17 +53,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.jmdns.JmDNS
|
|
||||||
import javax.jmdns.ServiceEvent
|
|
||||||
import javax.jmdns.ServiceListener
|
|
||||||
import javax.jmdns.ServiceTypeListener
|
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private var _jmDNS: JmDNS? = null;
|
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer(9999);
|
||||||
@@ -69,105 +78,51 @@ class StateCasting {
|
|||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||||
|
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
|
||||||
|
"_googlecast._tcp.local",
|
||||||
|
"_airplay._tcp.local",
|
||||||
|
"_fastcast._tcp.local",
|
||||||
|
"_fcast._tcp.local"
|
||||||
|
)) { handleServiceUpdated(it) }
|
||||||
|
|
||||||
val isCasting: Boolean get() = activeDevice != null;
|
val isCasting: Boolean get() = activeDevice != null;
|
||||||
|
|
||||||
private val _chromecastServiceListener = object : ServiceListener {
|
private fun handleServiceUpdated(services: List<DnsService>) {
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
for (s in services) {
|
||||||
Logger.i(TAG, "ChromeCast service added: " + event.info);
|
//TODO: Addresses IPv4 only?
|
||||||
addOrUpdateDevice(event);
|
val addresses = s.addresses.toTypedArray()
|
||||||
}
|
val port = s.port.toInt()
|
||||||
|
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
if (s.name.endsWith("._googlecast._tcp.local")) {
|
||||||
Logger.i(TAG, "ChromeCast service removed: " + event.info);
|
if (name == null) {
|
||||||
synchronized(devices) {
|
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateChromeCastDevice(name, addresses, port)
|
||||||
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateChromeCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _airPlayServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateAirPlayDevice(name, addresses, port)
|
||||||
Logger.i(TAG, "AirPlay service resolved: " + event.info);
|
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateAirPlayDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _fastCastServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
|
} else if (s.name.endsWith("._fcast._tcp.local")) {
|
||||||
|
if (name == null) {
|
||||||
|
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service resolved: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateFastCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _serviceTypeListener = object : ServiceTypeListener {
|
|
||||||
override fun serviceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun subTypeForServiceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Sub type for service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(context: Context, url: String) {
|
fun handleUrl(context: Context, url: String) {
|
||||||
@@ -236,29 +191,30 @@ class StateCasting {
|
|||||||
rememberedDevices.clear();
|
rememberedDevices.clear();
|
||||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
||||||
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
val jmDNS = JmDNS.create(InetAddress.getLocalHost());
|
|
||||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
_jmDNS = jmDNS;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to start casting service.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_castServer.start();
|
_castServer.start();
|
||||||
enableDeveloper(true);
|
enableDeveloper(true);
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService started.");
|
Logger.i(TAG, "CastingService started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun startDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.start()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun stopDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.stop()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
if (!_started)
|
if (!_started)
|
||||||
@@ -268,25 +224,7 @@ class StateCasting {
|
|||||||
|
|
||||||
Logger.i(TAG, "CastingService stopping.")
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
val jmDNS = _jmDNS;
|
stopDiscovering()
|
||||||
if (jmDNS != null) {
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
jmDNS.close();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to stop mDNS.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_scopeIO.cancel();
|
_scopeIO.cancel();
|
||||||
_scopeMain.cancel();
|
_scopeMain.cancel();
|
||||||
|
|
||||||
@@ -436,15 +374,26 @@ class StateCasting {
|
|||||||
} else {
|
} else {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (ad is FCastCastingDevice) {
|
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||||
Logger.i(TAG, "Casting as DASH direct");
|
if (isRawDash) {
|
||||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as raw DASH");
|
||||||
} else if (ad is AirPlayCastingDevice) {
|
|
||||||
Logger.i(TAG, "Casting as HLS indirect");
|
try {
|
||||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as DASH indirect");
|
if (ad is FCastCastingDevice) {
|
||||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as DASH direct");
|
||||||
|
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else if (ad is AirPlayCastingDevice) {
|
||||||
|
Logger.i(TAG, "Casting as HLS indirect");
|
||||||
|
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Casting as DASH indirect");
|
||||||
|
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
@@ -452,14 +401,22 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||||
Logger.i(TAG, "Casting as singular video");
|
Logger.i(TAG, "Casting as singular video");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if (audioSource is IAudioUrlSource) {
|
} else if (audioSource is IAudioUrlSource) {
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
||||||
Logger.i(TAG, "Casting as singular audio");
|
Logger.i(TAG, "Casting as singular audio");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
} else if(videoSource is IHLSManifestSource) {
|
} else if(videoSource is IHLSManifestSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
Logger.i(TAG, "Casting as proxied HLS");
|
||||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
@@ -467,7 +424,7 @@ class StateCasting {
|
|||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||||
}
|
}
|
||||||
} else if(audioSource is IHLSManifestAudioSource) {
|
} else if(audioSource is IHLSManifestAudioSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||||
} else {
|
} else {
|
||||||
@@ -480,6 +437,26 @@ class StateCasting {
|
|||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||||
|
} else if (videoSource is JSDashManifestRawSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var str = listOf(
|
var str = listOf(
|
||||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
@@ -520,7 +497,7 @@ class StateCasting {
|
|||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
@@ -539,7 +516,7 @@ class StateCasting {
|
|||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
@@ -558,7 +535,7 @@ class StateCasting {
|
|||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -654,7 +631,7 @@ class StateCasting {
|
|||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -667,8 +644,11 @@ class StateCasting {
|
|||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
val subtitleUrl = url + subtitlePath;
|
val subtitleUrl = url + subtitlePath;
|
||||||
|
|
||||||
|
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
"application/dash+xml")
|
"application/dash+xml")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -699,13 +679,17 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val subtitlePath = "/subtitle-${id}";
|
|
||||||
|
|
||||||
val videoUrl = videoSource?.getVideoUrl();
|
val videoPath = "/video-${id}"
|
||||||
val audioUrl = audioSource?.getAudioUrl();
|
val audioPath = "/audio-${id}"
|
||||||
|
val subtitlePath = "/subtitle-${id}"
|
||||||
|
|
||||||
|
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
|
||||||
|
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
|
||||||
|
|
||||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
return@withContext subtitleSource.getSubtitlesURI();
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
@@ -734,26 +718,42 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (videoSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
if (audioSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||||
|
|
||||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
||||||
|
Logger.v(TAG) { "Dash manifest: $content" };
|
||||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
val hlsUrl = url + hlsPath
|
val hlsUrl = url + hlsPath
|
||||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
|
||||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||||
|
|
||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
@@ -780,7 +780,7 @@ class StateCasting {
|
|||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFuntionHandler
|
return@HttpFunctionHandler
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -797,7 +797,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@@ -827,7 +827,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
newPlaylistUrl = url + newPlaylistPath
|
newPlaylistUrl = url + newPlaylistPath
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@@ -916,7 +916,7 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -1044,9 +1044,9 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -1090,8 +1090,11 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
"application/dash+xml")
|
"application/dash+xml")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -1117,6 +1120,166 @@ class StateCasting {
|
|||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanExecutors() {
|
||||||
|
if (_videoExecutor != null) {
|
||||||
|
_videoExecutor?.cleanup()
|
||||||
|
_videoExecutor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_audioExecutor != null) {
|
||||||
|
_audioExecutor?.cleanup()
|
||||||
|
_audioExecutor = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
|
cleanExecutors()
|
||||||
|
_castServer.removeAllHandlers("castDashRaw")
|
||||||
|
|
||||||
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
val dashPath = "/dash-${id}"
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val subtitlePath = "/subtitle-${id}"
|
||||||
|
|
||||||
|
val dashUrl = url + dashPath;
|
||||||
|
Logger.i(TAG, "DASH url: $dashUrl");
|
||||||
|
|
||||||
|
val videoUrl = url + videoPath
|
||||||
|
val audioUrl = url + audioPath
|
||||||
|
|
||||||
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
var subtitlesUrl: String? = null;
|
||||||
|
if (subtitlesUri != null) {
|
||||||
|
if(subtitlesUri.scheme == "file") {
|
||||||
|
var content: String? = null;
|
||||||
|
val inputStream = contentResolver.openInputStream(subtitlesUri);
|
||||||
|
inputStream?.use { stream ->
|
||||||
|
val reader = stream.bufferedReader();
|
||||||
|
content = reader.use { it.readText() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitlesUrl = url + subtitlePath;
|
||||||
|
} else {
|
||||||
|
subtitlesUrl = subtitlesUri.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashContent = withContext(Dispatchers.IO) {
|
||||||
|
//TODO: Include subtitlesURl in the future
|
||||||
|
return@withContext if (audioSource != null && videoSource != null) {
|
||||||
|
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||||
|
} else if (audioSource != null) {
|
||||||
|
audioSource.generate()
|
||||||
|
} else if (videoSource != null) {
|
||||||
|
videoSource.generate()
|
||||||
|
} else {
|
||||||
|
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: throw Exception("Dash is null")
|
||||||
|
|
||||||
|
for (representation in representationRegex.findAll(dashContent)) {
|
||||||
|
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||||
|
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||||
|
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||||
|
return@replace it.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType.startsWith("video/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else if (mediaType.startsWith("audio/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected audio or video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && !videoSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Video source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && !audioSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Audio source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && audioSource.hasRequestExecutor) {
|
||||||
|
_audioExecutor = audioSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||||
|
_videoExecutor = videoSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||||
|
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
|
"application/dash+xml")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
|
||||||
|
if (videoSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", videoPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val videoExecutor = _videoExecutor;
|
||||||
|
if (videoExecutor != null) {
|
||||||
|
val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
if (audioSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val audioExecutor = _audioExecutor;
|
||||||
|
if (audioExecutor != null) {
|
||||||
|
val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||||
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
return when (deviceInfo.type) {
|
return when (deviceInfo.type) {
|
||||||
CastProtocolType.CHROMECAST -> {
|
CastProtocolType.CHROMECAST -> {
|
||||||
@@ -1211,7 +1374,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val newDevice = deviceFactory();
|
val newDevice = deviceFactory();
|
||||||
devices[name] = newDevice;
|
this.devices[name] = newDevice;
|
||||||
|
|
||||||
invokeEvents = {
|
invokeEvents = {
|
||||||
onDeviceAdded.emit(newDevice);
|
onDeviceAdded.emit(newDevice);
|
||||||
@@ -1225,7 +1388,7 @@ class StateCasting {
|
|||||||
fun enableDeveloper(enableDev: Boolean){
|
fun enableDeveloper(enableDev: Boolean){
|
||||||
_castServer.removeAllHandlers("dev");
|
_castServer.removeAllHandlers("dev");
|
||||||
if(enableDev) {
|
if(enableDev) {
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
|
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
|
||||||
if (context.query.containsKey("dashUrl")) {
|
if (context.query.containsKey("dashUrl")) {
|
||||||
val dashUrl = context.query["dashUrl"];
|
val dashUrl = context.query["dashUrl"];
|
||||||
val html = "<div>\n" +
|
val html = "<div>\n" +
|
||||||
@@ -1265,6 +1428,9 @@ class StateCasting {
|
|||||||
companion object {
|
companion object {
|
||||||
val instance: StateCasting = StateCasting();
|
val instance: StateCasting = StateCasting();
|
||||||
|
|
||||||
|
private val representationRegex = Regex("<Representation .*?mimeType=\"(.*?)\".*?>(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
private val TAG = "StateCasting";
|
private val TAG = "StateCasting";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ import com.futo.platformplayer.states.StateDeveloper
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.google.gson.ExclusionStrategy
|
import com.google.gson.ExclusionStrategy
|
||||||
import com.google.gson.FieldAttributes
|
import com.google.gson.FieldAttributes
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -573,7 +571,7 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val resp = _client.get(body.url!!, body.headers);
|
val resp = _client.get(body.url!!, body.headers);
|
||||||
|
|
||||||
context.respondCode(200,
|
context.respondCode(200,
|
||||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
|
||||||
context.query.getOrDefault("CT", "text/plain"));
|
context.query.getOrDefault("CT", "text/plain"));
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
private lateinit var _buttonCancel: ImageButton;
|
private lateinit var _buttonCancel: ImageButton;
|
||||||
|
|
||||||
private lateinit var _editPassword: EditText;
|
private lateinit var _editPassword: EditText;
|
||||||
|
private lateinit var _editPassword2: EditText;
|
||||||
|
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
private lateinit var _inputMethodManager: InputMethodManager;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStart = findViewById(R.id.button_start);
|
_buttonStart = findViewById(R.id.button_start);
|
||||||
_editPassword = findViewById(R.id.edit_password);
|
_editPassword = findViewById(R.id.edit_password);
|
||||||
|
_editPassword2 = findViewById(R.id.edit_password2);
|
||||||
|
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
@@ -52,6 +54,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonStart.setOnClickListener {
|
_buttonStart.setOnClickListener {
|
||||||
|
val p1 = _editPassword.text.toString();
|
||||||
|
val p2 = _editPassword2.text.toString();
|
||||||
|
if(!(p1?.equals(p2) ?: false)) {
|
||||||
|
UIDialogs.toast(context, "Password fields do not match, confirm that you typed it correctly.");
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
val pbytes = _editPassword.text.toString().toByteArray();
|
val pbytes = _editPassword.text.toString().toByteArray();
|
||||||
if(pbytes.size < 4 || pbytes.size > 32) {
|
if(pbytes.size < 4 || pbytes.size > 32) {
|
||||||
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
_textCharacterCount = findViewById(R.id.character_count);
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
handleCloseAttempt()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_editComment.addTextChangedListener(object : TextWatcher {
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) = Unit
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||||
|
val count = s?.length ?: 0;
|
||||||
_textCharacterCount.text = count.toString();
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
@@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
clearFocus();
|
handleCloseAttempt()
|
||||||
dismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOnCancelListener {
|
||||||
|
handleCloseAttempt()
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
@@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseAttempt() {
|
||||||
|
if (_editComment.text.isEmpty()) {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(
|
||||||
|
context,
|
||||||
|
context.resources.getString(R.string.not_empty_close),
|
||||||
|
action = {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun focus() {
|
private fun focus() {
|
||||||
_editComment.requestFocus();
|
_editComment.requestFocus();
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
|
StateCasting.instance.startDiscovering()
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
_devices.clear();
|
_devices.clear();
|
||||||
@@ -169,6 +171,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.stop();
|
(_imageLoader.drawable as Animatable?)?.stop();
|
||||||
|
|
||||||
|
StateCasting.instance.stopDiscovering()
|
||||||
StateCasting.instance.onDeviceAdded.remove(this);
|
StateCasting.instance.onDeviceAdded.remove(this);
|
||||||
StateCasting.instance.onDeviceChanged.remove(this);
|
StateCasting.instance.onDeviceChanged.remove(this);
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
StateCasting.instance.onDeviceRemoved.remove(this);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescri
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
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.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
@@ -25,6 +27,14 @@ 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.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
|
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.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
@@ -34,6 +44,7 @@ import com.futo.platformplayer.parsers.HLS
|
|||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
import hasAnySource
|
import hasAnySource
|
||||||
@@ -46,9 +57,12 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.Thread.sleep
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -56,6 +70,7 @@ import java.util.concurrent.ForkJoinPool
|
|||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.time.times
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class VideoDownload {
|
class VideoDownload {
|
||||||
@@ -71,12 +86,51 @@ class VideoDownload {
|
|||||||
|
|
||||||
var targetPixelCount: Long? = null;
|
var targetPixelCount: Long? = null;
|
||||||
var targetBitrate: Long? = null;
|
var targetBitrate: Long? = null;
|
||||||
|
var targetVideoName: String? = null;
|
||||||
|
var targetAudioName: String? = null;
|
||||||
|
|
||||||
var videoSource: VideoUrlSource?;
|
var videoSource: VideoUrlSource?;
|
||||||
var audioSource: AudioUrlSource?;
|
var audioSource: AudioUrlSource?;
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val audioSourceToUse: IAudioSource? get () = if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
|
||||||
|
|
||||||
|
var requireVideoSource: Boolean = false;
|
||||||
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val isVideoDownloadReady: Boolean get() = !requireVideoSource ||
|
||||||
|
((requiresLiveVideoSource && isLiveVideoSourceValid) || (!requiresLiveVideoSource && videoSource != null));
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val isAudioDownloadReady: Boolean get() = !requireAudioSource ||
|
||||||
|
((requiresLiveAudioSource && isLiveAudioSourceValid) || (!requiresLiveAudioSource && audioSource != null));
|
||||||
|
|
||||||
|
|
||||||
var subtitleSource: SubtitleRawSource?;
|
var subtitleSource: SubtitleRawSource?;
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
var prepareTime: OffsetDateTime? = null;
|
var prepareTime: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
var requiresLiveVideoSource: Boolean = false;
|
||||||
|
@Contextual
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
var videoSourceLive: JSSource? = null;
|
||||||
|
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||||
|
|
||||||
|
var requiresLiveAudioSource: Boolean = false;
|
||||||
|
@Contextual
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
var audioSourceLive: JSSource? = null;
|
||||||
|
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||||
|
|
||||||
|
var hasVideoRequestExecutor: Boolean = false;
|
||||||
|
var hasAudioRequestExecutor: Boolean = false;
|
||||||
|
|
||||||
var progress: Double = 0.0;
|
var progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
var isCancelled = false;
|
||||||
|
|
||||||
@@ -111,21 +165,40 @@ class VideoDownload {
|
|||||||
onStateChanged.emit(newState);
|
onStateChanged.emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoSource = null;
|
this.videoSource = null;
|
||||||
this.audioSource = null;
|
this.audioSource = null;
|
||||||
this.subtitleSource = null;
|
this.subtitleSource = null;
|
||||||
this.targetPixelCount = targetPixelCount;
|
this.targetPixelCount = targetPixelCount;
|
||||||
this.targetBitrate = targetBitrate;
|
this.targetBitrate = targetBitrate;
|
||||||
|
this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = false;
|
||||||
|
this.requiresLiveAudioSource = false;
|
||||||
|
this.targetVideoName = videoSource?.name;
|
||||||
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||||
this.videoSource = VideoUrlSource.fromUrlSource(videoSource);
|
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||||
this.audioSource = AudioUrlSource.fromUrlSource(audioSource);
|
this.audioSource = if(audioSource is IAudioUrlSource) AudioUrlSource.fromUrlSource(audioSource) else null;
|
||||||
|
this.videoSourceLive = if(videoSource is JSSource) videoSource else null;
|
||||||
|
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
this.prepareTime = OffsetDateTime.now();
|
this.prepareTime = OffsetDateTime.now();
|
||||||
|
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||||
|
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||||
|
this.targetVideoName = videoSource?.name;
|
||||||
|
this.targetAudioName = audioSource?.name;
|
||||||
|
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||||
|
this.targetBitrate = if(audioSource != null) audioSource.bitrate.toLong() else null;
|
||||||
|
this.requireVideoSource = videoSource != null;
|
||||||
|
this.requireAudioSource = audioSource != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
||||||
@@ -156,9 +229,21 @@ class VideoDownload {
|
|||||||
|
|
||||||
suspend fun prepare(client: ManagedHttpClient) {
|
suspend fun prepare(client: ManagedHttpClient) {
|
||||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
||||||
|
|
||||||
|
//If live sources are required, ensure a live object is present
|
||||||
|
if(requiresLiveVideoSource && !isLiveVideoSourceValid) {
|
||||||
|
videoDetails = null;
|
||||||
|
videoSource = null;
|
||||||
|
videoSourceLive = null;
|
||||||
|
}
|
||||||
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
|
videoDetails = null;
|
||||||
|
audioSource = null;
|
||||||
|
videoSourceLive = null;
|
||||||
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null)
|
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null && targetVideoName == null && targetAudioName == null)
|
||||||
throw IllegalStateException("No sources or query values set");
|
throw IllegalStateException("No sources or query values set");
|
||||||
|
|
||||||
//Fetch full video object and determine source
|
//Fetch full video object and determine source
|
||||||
@@ -167,6 +252,30 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(requiredCheck) {
|
||||||
|
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||||
|
if(requireVideoSource) {
|
||||||
|
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||||
|
requireVideoSource = false;
|
||||||
|
targetPixelCount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requireAudioSource) {
|
||||||
|
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(requireAudioSource) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredCheck = false;
|
||||||
|
}
|
||||||
|
|
||||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
throw DownloadException("Unsupported video for downloading", false);
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
@@ -192,23 +301,35 @@ class VideoDownload {
|
|||||||
videoSources.add(source)
|
videoSources.add(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var vsource: IVideoSource? = null;
|
||||||
|
|
||||||
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
if(targetVideoName != null)
|
||||||
|
vsource = videoSources.find { x -> x.isDownloadable() && x.name == targetVideoName };
|
||||||
|
if(vsource == null && targetPixelCount == null)
|
||||||
|
throw IllegalStateException("Could not find comparable downloadable video stream (No target pixel count)");
|
||||||
|
if(vsource == null)
|
||||||
|
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
if(vsource != null) {
|
|
||||||
if (vsource is IVideoUrlSource)
|
if(vsource == null) {
|
||||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
videoSource = null;
|
||||||
else
|
if(original.video.videoSources.size == 0)
|
||||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
requireVideoSource = false;
|
||||||
}
|
}
|
||||||
|
else if(vsource is IVideoUrlSource)
|
||||||
|
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||||
|
else if(vsource is JSSource && requiresLiveVideoSource)
|
||||||
|
videoSourceLive = vsource;
|
||||||
|
else
|
||||||
|
throw DownloadException("Video source is not supported for downloading (yet) [" + vsource?.javaClass?.name + "]", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(audioSource == null && targetBitrate != null) {
|
if(audioSource == null && targetBitrate != null) {
|
||||||
val audioSources = arrayListOf<IAudioSource>()
|
var audioSources = mutableListOf<IAudioSource>()
|
||||||
val video = original.video
|
val video = original.video
|
||||||
if (video is VideoUnMuxedSourceDescriptor) {
|
if (video is VideoUnMuxedSourceDescriptor) {
|
||||||
for (source in video.audioSources) {
|
for (source in video.audioSources) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestAudioSource) {
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
@@ -226,25 +347,43 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
var asource: IAudioSource? = null;
|
||||||
?: if(videoSource != null ) null
|
if(targetAudioName != null) {
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
val filteredAudioSources = audioSources.filter { x -> x.isDownloadable() && x.name == targetAudioName }.toTypedArray();
|
||||||
|
if(filteredAudioSources.size == 1)
|
||||||
|
asource = filteredAudioSources.first();
|
||||||
|
else if(filteredAudioSources.size > 1)
|
||||||
|
audioSources = filteredAudioSources.toMutableList();
|
||||||
|
}
|
||||||
|
if(asource == null && targetBitrate == null)
|
||||||
|
throw IllegalStateException("Could not find comparable downloadable video stream (No target bitrate)");
|
||||||
if(asource == null)
|
if(asource == null)
|
||||||
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
|
?: if(videoSource != null ) null
|
||||||
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
|
requireVideoSource = false;
|
||||||
|
}
|
||||||
else if(asource is IAudioUrlSource)
|
else if(asource is IAudioUrlSource)
|
||||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||||
|
else if(asource is JSSource && requiresLiveAudioSource)
|
||||||
|
audioSourceLive = asource;
|
||||||
else
|
else
|
||||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null)
|
if(!isVideoDownloadReady)
|
||||||
throw DownloadException("No valid sources found for video/audio");
|
throw DownloadException("No valid sources found for video");
|
||||||
|
if(!isAudioDownloadReady)
|
||||||
|
throw DownloadException("No valid sources found for audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||||
Logger.i(TAG, "VideoDownload Download [${name}]");
|
Logger.i(TAG, "VideoDownload Download [${name}]");
|
||||||
if(videoDetails == null || (videoSource == null && audioSource == null))
|
if(videoDetails == null || (videoSourceToUse == null && audioSourceToUse == null))
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
||||||
|
|
||||||
@@ -253,12 +392,19 @@ class VideoDownload {
|
|||||||
|
|
||||||
if(isCancelled) throw CancellationException("Download got cancelled");
|
if(isCancelled) throw CancellationException("Download got cancelled");
|
||||||
|
|
||||||
if(videoSource != null) {
|
val actualVideoSource = if(requiresLiveVideoSource && videoSourceLive is IVideoSource)
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${videoSource!!.width}x${videoSource!!.height}].${videoContainerToExtension(videoSource!!.container)}".sanitizeFileName();
|
videoSourceLive as IVideoSource?;
|
||||||
|
else videoSource;
|
||||||
|
val actualAudioSource = if(requiresLiveAudioSource && audioSourceLive is IAudioSource)
|
||||||
|
audioSourceLive as IAudioSource?;
|
||||||
|
else audioSource;
|
||||||
|
|
||||||
|
if(actualVideoSource != null) {
|
||||||
|
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -273,10 +419,11 @@ class VideoDownload {
|
|||||||
var lastAudioLength: Long = 0;
|
var lastAudioLength: Long = 0;
|
||||||
var lastAudioRead: Long = 0;
|
var lastAudioRead: Long = 0;
|
||||||
|
|
||||||
if(videoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading video");
|
Logger.i(TAG, "Started downloading video");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastVideoLength = length;
|
lastVideoLength = length;
|
||||||
@@ -289,23 +436,34 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFileSize = when (videoSource!!.container) {
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
videoFileSize = when (videoSource!!.container) {
|
||||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
|
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading audio");
|
Logger.i(TAG, "Started downloading audio");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastAudioLength = length;
|
lastAudioLength = length;
|
||||||
@@ -318,17 +476,27 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioFileSize = when (audioSource!!.container) {
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
audioFileSize = when (audioSource!!.container) {
|
||||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
|
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
|
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (subtitleSource != null) {
|
if (subtitleSource != null) {
|
||||||
@@ -398,15 +566,20 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
||||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||||
segmentFiles.add(segmentFile)
|
val outputStream = segmentFile.outputStream()
|
||||||
|
try {
|
||||||
|
segmentFiles.add(segmentFile)
|
||||||
|
|
||||||
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
|
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadedTotalLength += segmentLength
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadedTotalLength += segmentLength
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Combining segments into $targetFile");
|
Logger.i(TAG, "Combining segments into $targetFile");
|
||||||
@@ -473,6 +646,86 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
|
if(targetFile.exists())
|
||||||
|
targetFile.delete();
|
||||||
|
|
||||||
|
targetFile.createNewFile();
|
||||||
|
|
||||||
|
val sourceLength: Long?;
|
||||||
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
|
||||||
|
try{
|
||||||
|
var manifest = source.manifest;
|
||||||
|
if(source.hasGenerate)
|
||||||
|
manifest = source.generate();
|
||||||
|
if(manifest == null)
|
||||||
|
throw IllegalStateException("No manifest after generation");
|
||||||
|
|
||||||
|
//TODO: Temporary naive assume single-sourced dash
|
||||||
|
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
|
||||||
|
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
|
||||||
|
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
||||||
|
val foundTemplateUrl = foundTemplate.groupValues[1];
|
||||||
|
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
|
||||||
|
if(foundCues.count() <= 0)
|
||||||
|
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||||
|
|
||||||
|
val executor = if(source is JSSource && source.hasRequestExecutor)
|
||||||
|
source.getRequestExecutor();
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
val speedTracker = SpeedTracker(1000);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
|
var written = 0;
|
||||||
|
var indexCounter = 0;
|
||||||
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
|
for(cue in foundCues) {
|
||||||
|
val t = cue.groupValues[1];
|
||||||
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
|
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||||
|
|
||||||
|
val data = if(executor != null)
|
||||||
|
executor.executeRequest("GET", url, null, mapOf());
|
||||||
|
else {
|
||||||
|
val resp = client.get(url, mutableMapOf());
|
||||||
|
if(!resp.isOk)
|
||||||
|
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||||
|
resp.body!!.bytes()
|
||||||
|
}
|
||||||
|
fileStream.write(data, 0, data.size);
|
||||||
|
speedTracker.addWork(data.size.toLong());
|
||||||
|
written += data.size;
|
||||||
|
|
||||||
|
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
|
||||||
|
|
||||||
|
indexCounter++;
|
||||||
|
}
|
||||||
|
sourceLength = written.toLong();
|
||||||
|
|
||||||
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
|
}
|
||||||
|
catch(ioex: IOException) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||||
|
throw Exception("Not enough space on device", ioex);
|
||||||
|
else
|
||||||
|
throw ioex;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fileStream.close();
|
||||||
|
}
|
||||||
|
return sourceLength!!;
|
||||||
|
}
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
@@ -484,17 +737,25 @@ class VideoDownload {
|
|||||||
|
|
||||||
try{
|
try{
|
||||||
val head = client.tryHead(videoUrl);
|
val head = client.tryHead(videoUrl);
|
||||||
|
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
||||||
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
{
|
{
|
||||||
val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
|
val maxParallel = if(relatedPlugin != null && relatedPlugin.config.maxDownloadParallelism > 0)
|
||||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})");
|
relatedPlugin.config.maxDownloadParallelism else 99;
|
||||||
|
val concurrency = Math.min(maxParallel, Settings.instance.downloads.getByteRangeThreadCount());
|
||||||
|
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
||||||
sourceLength = head["content-length"]!!.toLong();
|
sourceLength = head["content-length"]!!.toLong();
|
||||||
onProgress(sourceLength, 0, 0);
|
onProgress(sourceLength, 0, 0);
|
||||||
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Download $name Sequential");
|
Logger.i(TAG, "Download $name Sequential");
|
||||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
try {
|
||||||
|
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
@@ -518,17 +779,19 @@ class VideoDownload {
|
|||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
val progressRate: Int = 4096 * 25;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 25;
|
val speedRate: Int = 4096 * 5;
|
||||||
var readSinceLastSpeedTest: Long = 0;
|
var readSinceLastSpeedTest: Long = 0;
|
||||||
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = client.get(url);
|
val result = client.get(url);
|
||||||
if (!result.isOk)
|
if (!result.isOk) {
|
||||||
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
|
}
|
||||||
if (result.body == null)
|
if (result.body == null)
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
||||||
|
|
||||||
@@ -536,41 +799,114 @@ class VideoDownload {
|
|||||||
val sourceStream = result.body.byteStream();
|
val sourceStream = result.body.byteStream();
|
||||||
|
|
||||||
var totalRead: Long = 0;
|
var totalRead: Long = 0;
|
||||||
var read: Int;
|
try {
|
||||||
|
var read: Int;
|
||||||
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
do {
|
||||||
|
read = sourceStream.read(buffer);
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
do {
|
fileStream.write(buffer, 0, read);
|
||||||
read = sourceStream.read(buffer);
|
|
||||||
if (read < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
fileStream.write(buffer, 0, read);
|
totalRead += read;
|
||||||
|
|
||||||
totalRead += read;
|
readSinceLastSpeedTest += read;
|
||||||
|
if (totalRead.toDouble() / progressRate > lastProgressCount) {
|
||||||
|
onProgress(sourceLength, totalRead, lastSpeed);
|
||||||
|
lastProgressCount++;
|
||||||
|
}
|
||||||
|
if (readSinceLastSpeedTest > speedRate) {
|
||||||
|
val lastSpeedTime = timeSinceLastSpeedTest;
|
||||||
|
timeSinceLastSpeedTest = System.currentTimeMillis();
|
||||||
|
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
||||||
|
if (timeSince > 0)
|
||||||
|
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
||||||
|
readSinceLastSpeedTest = 0;
|
||||||
|
}
|
||||||
|
|
||||||
readSinceLastSpeedTest += read;
|
if (isCancelled)
|
||||||
if (totalRead / progressRate > lastProgressCount) {
|
throw CancellationException("Cancelled");
|
||||||
onProgress(sourceLength, totalRead, lastSpeed);
|
} while (read > 0);
|
||||||
lastProgressCount++;
|
} finally {
|
||||||
}
|
sourceStream.close()
|
||||||
if (readSinceLastSpeedTest > speedRate) {
|
result.body.close()
|
||||||
val lastSpeedTime = timeSinceLastSpeedTest;
|
}
|
||||||
timeSinceLastSpeedTest = System.currentTimeMillis();
|
|
||||||
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
|
||||||
if (timeSince > 0)
|
|
||||||
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
|
||||||
readSinceLastSpeedTest = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancelled)
|
|
||||||
throw CancellationException("Cancelled");
|
|
||||||
} while (read > 0);
|
|
||||||
|
|
||||||
lastSpeed = 0;
|
|
||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
return sourceLength;
|
return sourceLength;
|
||||||
}
|
}
|
||||||
|
/*private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
|
val progressRate: Int = 4096 * 25
|
||||||
|
var lastProgressCount: Int = 0
|
||||||
|
val speedRate: Int = 4096 * 25
|
||||||
|
var readSinceLastSpeedTest: Long = 0
|
||||||
|
var timeSinceLastSpeedTest: Long = System.currentTimeMillis()
|
||||||
|
|
||||||
|
var lastSpeed: Long = 0
|
||||||
|
|
||||||
|
var totalRead: Long = 0
|
||||||
|
var sourceLength: Long
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
|
||||||
|
var isPartialDownload = false
|
||||||
|
var result: ManagedHttpClient.Response? = null
|
||||||
|
do {
|
||||||
|
result = client.get(url, if (isPartialDownload) hashMapOf("Range" to "bytes=$totalRead-") else hashMapOf())
|
||||||
|
if (isPartialDownload) {
|
||||||
|
if (result.code != 206)
|
||||||
|
throw IllegalStateException("Failed to download source, byte range fallback failed. Web[${result.code}] Error")
|
||||||
|
} else {
|
||||||
|
if (!result.isOk)
|
||||||
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error")
|
||||||
|
}
|
||||||
|
if (result.body == null)
|
||||||
|
throw IllegalStateException("Failed to download source. Web[${result.code}] No response")
|
||||||
|
|
||||||
|
isPartialDownload = true
|
||||||
|
sourceLength = result.body!!.contentLength()
|
||||||
|
val sourceStream = result.body!!.byteStream()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
val read = sourceStream.read(buffer)
|
||||||
|
if (read <= 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream.write(buffer, 0, read)
|
||||||
|
|
||||||
|
totalRead += read
|
||||||
|
readSinceLastSpeedTest += read
|
||||||
|
|
||||||
|
if (totalRead / progressRate > lastProgressCount) {
|
||||||
|
onProgress(sourceLength, totalRead, lastSpeed)
|
||||||
|
lastProgressCount++
|
||||||
|
}
|
||||||
|
if (readSinceLastSpeedTest > speedRate) {
|
||||||
|
val lastSpeedTime = timeSinceLastSpeedTest
|
||||||
|
timeSinceLastSpeedTest = System.currentTimeMillis()
|
||||||
|
val timeSince = timeSinceLastSpeedTest - lastSpeedTime
|
||||||
|
if (timeSince > 0)
|
||||||
|
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong()
|
||||||
|
readSinceLastSpeedTest = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled)
|
||||||
|
throw CancellationException("Cancelled")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Sequential download was interrupted, trying to fallback to byte ranges", e)
|
||||||
|
} finally {
|
||||||
|
sourceStream.close()
|
||||||
|
result.body?.close()
|
||||||
|
}
|
||||||
|
} while (totalRead < sourceLength)
|
||||||
|
|
||||||
|
onProgress(sourceLength, totalRead, 0)
|
||||||
|
return sourceLength
|
||||||
|
}*/
|
||||||
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
||||||
val progressRate: Int = 4096 * 5;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
@@ -643,23 +979,47 @@ class VideoDownload {
|
|||||||
return tasks.map { it.get() };
|
return tasks.map { it.get() };
|
||||||
}
|
}
|
||||||
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
||||||
val toRead = rangeEnd - rangeStart;
|
var retryCount = 0
|
||||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
var lastException: Throwable? = null
|
||||||
if(!req.isOk)
|
|
||||||
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
|
||||||
if(req.body == null)
|
|
||||||
throw IllegalStateException("Range request failed, No body");
|
|
||||||
val read = req.body.contentLength();
|
|
||||||
|
|
||||||
if(read < toRead)
|
while (retryCount <= 3) {
|
||||||
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
try {
|
||||||
|
val toRead = rangeEnd - rangeStart;
|
||||||
|
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
||||||
|
if (!req.isOk) {
|
||||||
|
val bodyString = req.body?.string()
|
||||||
|
req.body?.close()
|
||||||
|
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
||||||
|
}
|
||||||
|
if (req.body == null)
|
||||||
|
throw IllegalStateException("Range request failed, No body");
|
||||||
|
val read = req.body.contentLength();
|
||||||
|
|
||||||
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
if (read < toRead)
|
||||||
|
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
||||||
|
|
||||||
|
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to download range (url=${url} bytes=${rangeStart}-${rangeEnd})", e)
|
||||||
|
|
||||||
|
retryCount++
|
||||||
|
lastException = e
|
||||||
|
|
||||||
|
sleep(when (retryCount) {
|
||||||
|
1 -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
2 -> 2000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
3 -> 4000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
else -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastException!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate() {
|
fun validate() {
|
||||||
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
||||||
if(videoSource != null) {
|
if(videoSourceToUse != null) {
|
||||||
if(videoFilePath == null)
|
if(videoFilePath == null)
|
||||||
throw IllegalStateException("Missing video file name after download");
|
throw IllegalStateException("Missing video file name after download");
|
||||||
val expectedFile = File(videoFilePath!!);
|
val expectedFile = File(videoFilePath!!);
|
||||||
@@ -670,7 +1030,7 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(audioSourceToUse != null) {
|
||||||
if(audioFilePath == null)
|
if(audioFilePath == null)
|
||||||
throw IllegalStateException("Missing audio file name after download");
|
throw IllegalStateException("Missing audio file name after download");
|
||||||
val expectedFile = File(audioFilePath!!);
|
val expectedFile = File(audioFilePath!!);
|
||||||
@@ -692,15 +1052,15 @@ class VideoDownload {
|
|||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSource!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSource!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSource != null && videoSource is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
localVideoSource.streamMetaData = (videoSource as IStreamMetaDataSource).streamMetaData;
|
localVideoSource.streamMetaData = (videoSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(localAudioSource != null && audioSource != null && audioSource is IStreamMetaDataSource)
|
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
|
||||||
localAudioSource.streamMetaData = (audioSource as IStreamMetaDataSource).streamMetaData;
|
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(existing != null) {
|
if(existing != null) {
|
||||||
existing.videoSerialized = videoDetails!!;
|
existing.videoSerialized = videoDetails!!;
|
||||||
@@ -757,6 +1117,9 @@ class VideoDownload {
|
|||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
const val GROUP_WATCHLATER= "WatchLater";
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
|
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4";
|
||||||
@@ -803,4 +1166,27 @@ class VideoDownload {
|
|||||||
return "subtitle";
|
return "subtitle";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SpeedTracker {
|
||||||
|
private val segmentStart: Long;
|
||||||
|
private val intervalMs: Long;
|
||||||
|
private var workDone: Long;
|
||||||
|
var lastSpeed: Long;
|
||||||
|
constructor(intervalMs: Long) {
|
||||||
|
segmentStart = System.currentTimeMillis();
|
||||||
|
this.intervalMs = intervalMs;
|
||||||
|
this.workDone = 0;
|
||||||
|
this.lastSpeed = 0;
|
||||||
|
}
|
||||||
|
fun addWork(work: Long) {
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if((now - segmentStart) > intervalMs)
|
||||||
|
{
|
||||||
|
lastSpeed = workDone;
|
||||||
|
workDone = 0;
|
||||||
|
}
|
||||||
|
workDone += work;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,37 @@
|
|||||||
package com.futo.platformplayer.downloads
|
package com.futo.platformplayer.downloads
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.arthenica.ffmpegkit.*
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.arthenica.ffmpegkit.LogCallback
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
|
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
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.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.io.*
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class VideoExport {
|
class VideoExport {
|
||||||
var state: State = State.QUEUED;
|
|
||||||
|
|
||||||
var videoLocal: VideoLocal;
|
var videoLocal: VideoLocal;
|
||||||
var videoSource: LocalVideoSource?;
|
var videoSource: LocalVideoSource?;
|
||||||
var audioSource: LocalAudioSource?;
|
var audioSource: LocalAudioSource?;
|
||||||
var subtitleSource: LocalSubtitleSource?;
|
var subtitleSource: LocalSubtitleSource?;
|
||||||
|
|
||||||
var progress: Double = 0.0;
|
|
||||||
var isCancelled = false;
|
|
||||||
|
|
||||||
var error: String? = null;
|
|
||||||
|
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val onStateChanged = Event1<State>();
|
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val onProgressChanged = Event1<Double>();
|
|
||||||
|
|
||||||
fun changeState(newState: State) {
|
|
||||||
state = newState;
|
|
||||||
onStateChanged.emit(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||||
this.videoLocal = videoLocal;
|
this.videoLocal = videoLocal;
|
||||||
this.videoSource = videoSource;
|
this.videoSource = videoSource;
|
||||||
@@ -50,8 +40,6 @@ class VideoExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
||||||
if(isCancelled) throw CancellationException("Export got cancelled");
|
|
||||||
|
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
@@ -107,7 +95,6 @@ class VideoExport {
|
|||||||
throw Exception("Cannot export when no audio or video source is set.");
|
throw Exception("Cannot export when no audio or video source is set.");
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgressChanged.emit(100.0);
|
|
||||||
return@coroutineScope outputFile;
|
return@coroutineScope outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
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.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.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
@@ -81,6 +82,8 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
|
||||||
fun toPlatformVideo() : IPlatformVideoDetails {
|
fun toPlatformVideo() : IPlatformVideoDetails {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.caoccao.javet.exceptions.JavetException
|
|||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
|
import com.caoccao.javet.interop.options.V8Flags
|
||||||
|
import com.caoccao.javet.interop.options.V8RuntimeOptions
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
@@ -133,9 +135,10 @@ class V8Plugin {
|
|||||||
synchronized(_runtimeLock) {
|
synchronized(_runtimeLock) {
|
||||||
if (_runtime != null)
|
if (_runtime != null)
|
||||||
return;
|
return;
|
||||||
|
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||||
val host = V8Host.getV8Instance();
|
val host = V8Host.getV8Instance();
|
||||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||||
|
|
||||||
_runtime = host.createV8Runtime(options);
|
_runtime = host.createV8Runtime(options);
|
||||||
if (!host.isIsolateCreated)
|
if (!host.isIsolateCreated)
|
||||||
throw IllegalStateException("Isolate not created");
|
throw IllegalStateException("Isolate not created");
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.packages
|
|||||||
|
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
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.JSClientConstants
|
||||||
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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -49,9 +51,20 @@ class PackageBridge : V8Package {
|
|||||||
fun buildFlavor(): String {
|
fun buildFlavor(): String {
|
||||||
return BuildConfig.FLAVOR;
|
return BuildConfig.FLAVOR;
|
||||||
}
|
}
|
||||||
|
@V8Property
|
||||||
|
fun buildSpecVersion(): Int {
|
||||||
|
return JSClientConstants.PLUGIN_SPEC_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun dispose(value: V8Value) {
|
||||||
|
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||||
|
value.close();
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
|
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
UIDialogs.toast(str);
|
UIDialogs.toast(str);
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ class PackageDOMParser : V8Package {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun parentElement(): DOMNode? {
|
||||||
|
return parentNode();
|
||||||
|
}
|
||||||
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
|
|||||||
@V8Property
|
@V8Property
|
||||||
fun textContent(): String = _element.text();
|
fun textContent(): String = _element.text();
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun tagName(): String = _element.tagName().uppercase();
|
||||||
|
@V8Property
|
||||||
fun text(): String = _element.text().ifEmpty { data() };
|
fun text(): String = _element.text().ifEmpty { data() };
|
||||||
@V8Property
|
@V8Property
|
||||||
fun data(): String = _element.data();
|
fun data(): String = _element.data();
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import com.caoccao.javet.enums.V8ConversionMode
|
|||||||
import com.caoccao.javet.enums.V8ProxyMode
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
@@ -16,6 +20,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
@@ -64,33 +71,44 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.request(method, url, headers)
|
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.request(method, url, headers);
|
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.requestWithBody(method, url, body, headers)
|
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.requestWithBody(method, url, body, headers);
|
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.GET(url, headers)
|
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.GET(url, headers);
|
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
|
||||||
_packageClientAuth.POST(url, body, headers)
|
val client = if(useAuth) _packageClientAuth else _packageClient;
|
||||||
|
|
||||||
|
if(body is V8ValueString)
|
||||||
|
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is String)
|
||||||
|
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is V8ValueTypedArray)
|
||||||
|
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is ByteArray)
|
||||||
|
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
||||||
|
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else
|
else
|
||||||
_packageClient.POST(url, body, headers);
|
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -111,8 +129,19 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IBridgeHttpResponse {
|
||||||
|
val url: String;
|
||||||
|
val code: Int;
|
||||||
|
val headers: Map<String, List<String>>?;
|
||||||
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
class BridgeHttpStringResponse(
|
||||||
|
override val url: String,
|
||||||
|
override val code: Int, val
|
||||||
|
body: String?,
|
||||||
|
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
|
||||||
|
|
||||||
val isOk = code >= 200 && code < 300;
|
val isOk = code >= 200 && code < 300;
|
||||||
|
|
||||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
@@ -125,6 +154,37 @@ class PackageHttp: V8Package {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class BridgeHttpBytesResponse: IV8Convertable, IBridgeHttpResponse {
|
||||||
|
override val url: String;
|
||||||
|
override val code: Int;
|
||||||
|
val body: ByteArray?;
|
||||||
|
override val headers: Map<String, List<String>>?;
|
||||||
|
|
||||||
|
val isOk: Boolean;
|
||||||
|
|
||||||
|
constructor(url: String, code: Int, body: ByteArray? = null, headers: Map<String, List<String>>? = null) {
|
||||||
|
this.url = url;
|
||||||
|
this.code = code;
|
||||||
|
this.body = body;
|
||||||
|
this.headers = headers;
|
||||||
|
this.isOk = code >= 200 && code < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
|
val obj = runtime.createV8ValueObject();
|
||||||
|
obj.set("url", url);
|
||||||
|
obj.set("code", code);
|
||||||
|
if(body != null) {
|
||||||
|
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
||||||
|
buffer.fromBytes(body);
|
||||||
|
obj.set("body", body);
|
||||||
|
}
|
||||||
|
obj.set("headers", headers);
|
||||||
|
obj.set("isOk", isOk);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
||||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
@@ -147,6 +207,12 @@ class PackageHttp: V8Package {
|
|||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
||||||
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun DUMMY(): BatchBuilder {
|
||||||
|
_reqs.add(Pair(_package.getDefaultClient(false), RequestDescriptor("DUMMY", "", mutableMapOf())));
|
||||||
|
return BatchBuilder(_package, _reqs);
|
||||||
|
}
|
||||||
|
|
||||||
//Client-specific
|
//Client-specific
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -169,12 +235,14 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<BridgeHttpResponse> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _reqs.parallelStream().map {
|
||||||
|
if(it.second.method == "DUMMY")
|
||||||
|
return@map null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.toList();
|
.toList();
|
||||||
@@ -230,65 +298,116 @@ class PackageHttp: V8Package {
|
|||||||
if(_client is JSHttpClient)
|
if(_client is JSHttpClient)
|
||||||
_client.doAllowNewCookies = allow;
|
_client.doAllowNewCookies = allow;
|
||||||
}
|
}
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(timeoutMs: Int) {
|
||||||
|
if(_client is JSHttpClient) {
|
||||||
|
_client.setTimeout(timeoutMs.toLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
return@logExceptions catchHttp {
|
return@logExceptions catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, null);
|
//logRequest(method, url, headers, null);
|
||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
return@catchHttp when(returnType) {
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, body);
|
//logRequest(method, url, headers, body);
|
||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("GET", url, headers, null);
|
//logRequest("GET", url, headers, null);
|
||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
//val responseBody = resp.body?.string();
|
||||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("POST", url, headers, body);
|
//logRequest("POST", url, headers, body);
|
||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
//val responseBody = resp.body?.string();
|
||||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers);
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val client = _client;
|
||||||
|
//logRequest("POST", url, headers, body);
|
||||||
|
val resp = client.post(url, body, headers);
|
||||||
|
//val responseBody = resp.body?.string();
|
||||||
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
|
|
||||||
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -388,13 +507,13 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,20 +633,25 @@ class PackageHttp: V8Package {
|
|||||||
val url: String,
|
val url: String,
|
||||||
val headers: MutableMap<String, String>,
|
val headers: MutableMap<String, String>,
|
||||||
val body: String? = null,
|
val body: String? = null,
|
||||||
val contentType: String? = null
|
val contentType: String? = null,
|
||||||
|
val respType: ReturnType = ReturnType.STRING
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ReturnType(val value: Int) {
|
||||||
|
STRING(0),
|
||||||
|
BYTES(1);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageHttp";
|
private const val TAG = "PackageHttp";
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
Logger.w(TAG, "Failed to parse claim=$c", e)
|
Logger.w(TAG, "Failed to parse claim=$c", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!map.containsKey("Harbor"))
|
||||||
|
this.context?.let {
|
||||||
|
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
||||||
|
}
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|||||||
+15
-6
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -15,7 +16,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
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.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -41,10 +41,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _llmVideo: LinearLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||||
private var _pager: IPager<IPlatformContent>? = null;
|
private var _pager: IPager<IPlatformContent>? = null;
|
||||||
@@ -118,7 +119,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return;
|
val recyclerResults = _recyclerResults ?: return;
|
||||||
val llmVideo = _llmVideo ?: return;
|
val llmVideo = _glmVideo ?: return;
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount;
|
val visibleItemCount = recyclerResults.childCount;
|
||||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||||
@@ -163,9 +164,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmVideo = LinearLayoutManager(view.context);
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||||
_recyclerResults?.adapter = _adapterResults;
|
_recyclerResults?.adapter = _adapterResults;
|
||||||
_recyclerResults?.layoutManager = _llmVideo;
|
_recyclerResults?.layoutManager = _glmVideo;
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
@@ -181,6 +183,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel();
|
_nextPageHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmVideo?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||||
|
|||||||
+15
-5
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -36,10 +37,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null
|
private var _recyclerResults: RecyclerView? = null
|
||||||
private var _llmPlaylist: LinearLayoutManager? = null
|
private var _glmPlaylist: GridLayoutManager? = null
|
||||||
private var _loading = false
|
private var _loading = false
|
||||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||||
@@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return
|
val recyclerResults = _recyclerResults ?: return
|
||||||
val llmPlaylist = _llmPlaylist ?: return
|
val llmPlaylist = _glmPlaylist ?: return
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount
|
val visibleItemCount = recyclerResults.childCount
|
||||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||||
@@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmPlaylist = LinearLayoutManager(view.context)
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||||
_recyclerResults?.adapter = _adapterResults
|
_recyclerResults?.adapter = _adapterResults
|
||||||
_recyclerResults?.layoutManager = _llmPlaylist
|
_recyclerResults?.layoutManager = _glmPlaylist
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel()
|
_nextPageHandler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmPlaylist?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setPager(
|
private fun setPager(
|
||||||
pager: IPager<IPlatformPlaylist>
|
pager: IPager<IPlatformPlaylist>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+64
-15
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -16,6 +17,7 @@ import androidx.core.animation.doOnEnd
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
@@ -33,7 +35,7 @@ import kotlin.math.roundToInt
|
|||||||
class MenuBottomBarFragment : MainActivityFragment() {
|
class MenuBottomBarFragment : MainActivityFragment() {
|
||||||
private var _view: MenuBottomBarView? = null;
|
private var _view: MenuBottomBarView? = null;
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = MenuBottomBarView(this, inflater);
|
val view = MenuBottomBarView(this, inflater);
|
||||||
_view = view;
|
_view = view;
|
||||||
return view;
|
return view;
|
||||||
@@ -55,7 +57,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
return _view?.onBackPressed() ?: false;
|
return _view?.onBackPressed() ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_view?.updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
class MenuBottomBarView : LinearLayout {
|
class MenuBottomBarView : LinearLayout {
|
||||||
private val _fragment: MenuBottomBarFragment;
|
private val _fragment: MenuBottomBarFragment;
|
||||||
private val _inflater: LayoutInflater;
|
private val _inflater: LayoutInflater;
|
||||||
@@ -75,7 +83,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
private var _buttonsVisible = 0;
|
private var _buttonsVisible = 0;
|
||||||
private var _subscriptionsVisible = true;
|
private var _subscriptionsVisible = true;
|
||||||
|
|
||||||
var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
||||||
|
|
||||||
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
@@ -131,7 +139,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val staggerFactor = 3.0f
|
val staggerFactor = 3.0f
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
moreOverlay.visibility = LinearLayout.VISIBLE
|
moreOverlay.visibility = VISIBLE
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
|
||||||
@@ -160,7 +168,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
animatorSet.doOnEnd {
|
animatorSet.doOnEnd {
|
||||||
_moreVisibleAnimating = false
|
_moreVisibleAnimating = false
|
||||||
_moreVisible = false
|
_moreVisible = false
|
||||||
moreOverlay.visibility = LinearLayout.INVISIBLE
|
moreOverlay.visibility = INVISIBLE
|
||||||
}
|
}
|
||||||
animatorSet.playTogether(animations)
|
animatorSet.playTogether(animations)
|
||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
@@ -177,7 +185,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_layoutBottomBarButtons.removeAllViews();
|
_layoutBottomBarButtons.removeAllViews();
|
||||||
|
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
|
|
||||||
for ((index, button) in buttons.withIndex()) {
|
for ((index, button) in buttons.withIndex()) {
|
||||||
@@ -191,7 +199,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_layoutBottomBarButtons.addView(menuButton)
|
_layoutBottomBarButtons.addView(menuButton)
|
||||||
if (index < buttonDefinitions.size - 1) {
|
if (index < buttonDefinitions.size - 1) {
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +207,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,19 +216,30 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_moreButtons.clear();
|
_moreButtons.clear();
|
||||||
_layoutMoreButtons.removeAllViews();
|
_layoutMoreButtons.removeAllViews();
|
||||||
|
|
||||||
|
var insertedButtons = 0;
|
||||||
//Force buy to be on top for more buttons
|
//Force buy to be on top for more buttons
|
||||||
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
||||||
if (buyIndex != -1) {
|
if (buyIndex != -1) {
|
||||||
val button = buttons[buyIndex]
|
val button = buttons[buyIndex]
|
||||||
buttons.removeAt(buyIndex)
|
buttons.removeAt(buyIndex)
|
||||||
buttons.add(0, button)
|
buttons.add(0, button)
|
||||||
|
insertedButtons++;
|
||||||
}
|
}
|
||||||
//Force faq to be second
|
//Force faq to be second
|
||||||
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
||||||
if (faqIndex != -1) {
|
if (faqIndex != -1) {
|
||||||
val button = buttons[faqIndex]
|
val button = buttons[faqIndex]
|
||||||
buttons.removeAt(faqIndex)
|
buttons.removeAt(faqIndex)
|
||||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
buttons.add(if (insertedButtons == 1) 1 else 0, button)
|
||||||
|
insertedButtons++;
|
||||||
|
}
|
||||||
|
//Force privacy to be third
|
||||||
|
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
||||||
|
if (privacyIndex != -1) {
|
||||||
|
val button = buttons[privacyIndex]
|
||||||
|
buttons.removeAt(privacyIndex)
|
||||||
|
buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button)
|
||||||
|
insertedButtons++;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (data in buttons) {
|
for (data in buttons) {
|
||||||
@@ -243,9 +262,20 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
fun updateAllButtonVisibility() {
|
fun updateAllButtonVisibility() {
|
||||||
|
// if the more fly-out menu is open the we should close it
|
||||||
|
if(_moreVisible) {
|
||||||
|
setMoreVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = resources.displayMetrics
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible >= defs.size) {
|
if (_buttonsVisible >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
@@ -302,9 +332,6 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (!StatePayment.instance.hasPaid) {
|
if (!StatePayment.instance.hasPaid) {
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||||
}
|
}
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
|
||||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
|
||||||
}))
|
|
||||||
|
|
||||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||||
|
|
||||||
@@ -350,7 +377,15 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
//Add configurable buttons here
|
//Add configurable buttons here
|
||||||
var buttonDefinitions = listOf(
|
var buttonDefinitions = listOf(
|
||||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, { it.navigate<HomeFragment>() }),
|
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
||||||
|
val currentMain = it.currentMain
|
||||||
|
if (currentMain is HomeFragment) {
|
||||||
|
currentMain.scrollToTop(false)
|
||||||
|
currentMain.reloadFeed()
|
||||||
|
} else {
|
||||||
|
it.navigate<HomeFragment>()
|
||||||
|
}
|
||||||
|
}),
|
||||||
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
|
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
|
||||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
|
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
|
||||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
|
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
|
||||||
@@ -369,8 +404,22 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
||||||
|
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||||
|
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
StateApp.instance.setPrivacyMode(false);
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
StateApp.instance.setPrivacyMode(true);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}),
|
||||||
|
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
|
||||||
|
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||||
})
|
})
|
||||||
//98 is reversed for buy button
|
//96 is reserved for privacy button
|
||||||
|
//98 is reserved for buy button
|
||||||
//99 is reserved for more button
|
//99 is reserved for more button
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.futopay.PaymentConfigurations
|
import com.futo.futopay.PaymentConfigurations
|
||||||
import com.futo.futopay.PaymentManager
|
import com.futo.futopay.PaymentManager
|
||||||
|
import com.futo.futopay.formatMoney
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
@@ -89,14 +90,13 @@ class BuyFragment : MainFragment() {
|
|||||||
try {
|
try {
|
||||||
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
||||||
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
||||||
val country = StatePayment.instance.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
||||||
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
||||||
|
|
||||||
if(currency != null && prices.containsKey(currency.id)) {
|
if(currency != null && prices.containsKey(currency.id)) {
|
||||||
val price = prices[currency.id]!!;
|
val price = prices[currency.id]!!;
|
||||||
val priceDecimal = (price.toDouble() / 100);
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal) + context.getString(R.string.plus_tax);
|
_buttonBuyText.text = formatMoney(country.id, currency.id, price) + context.getString(R.string.plus_tax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -12,6 +15,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@@ -52,7 +56,9 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
|||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.polycentric.core.OwnedClaim
|
import com.futo.polycentric.core.OwnedClaim
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
@@ -64,7 +70,13 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class PolycentricProfile(
|
data class PolycentricProfile(
|
||||||
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
||||||
)
|
) {
|
||||||
|
fun getHarborUrl(context: Context): String{
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
|
||||||
|
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
||||||
|
return "https://harbor.social/" + url.substring("polycentric://".length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChannelFragment : MainFragment() {
|
class ChannelFragment : MainFragment() {
|
||||||
override val isMainView: Boolean = true
|
override val isMainView: Boolean = true
|
||||||
@@ -204,7 +216,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is IPlatformPlaylist -> {
|
is IPlatformPlaylist -> {
|
||||||
fragment.navigate<PlaylistFragment>(v)
|
fragment.navigate<RemotePlaylistFragment>(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
is IPlatformPost -> {
|
is IPlatformPost -> {
|
||||||
@@ -225,11 +237,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||||
SerializedPlatformVideo.fromVideo(
|
|
||||||
content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -221,8 +221,8 @@ class CommentsFragment : MainFragment() {
|
|||||||
|
|
||||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-39
@@ -4,31 +4,36 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
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.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
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.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.withTimestamp
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||||
private var _exoPlayer: PlayerManager? = null;
|
private var _exoPlayer: PlayerManager? = null;
|
||||||
@@ -41,9 +46,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
||||||
protected open val shouldShowTimeBar: Boolean get() = true
|
protected open val shouldShowTimeBar: Boolean get() = true
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return results;
|
return results;
|
||||||
@@ -51,16 +54,10 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
|
|
||||||
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
||||||
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
||||||
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
|
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
val v = LinearLayout(context).apply {
|
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
orientation = LinearLayout.VERTICAL;
|
|
||||||
};
|
|
||||||
headerView = v;
|
|
||||||
|
|
||||||
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
|
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +82,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
};
|
};
|
||||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -114,8 +111,13 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
|
|
||||||
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
|
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(
|
||||||
{ StateMeta.instance.addHiddenVideo(content.url);
|
context,
|
||||||
|
R.drawable.ic_visibility_off,
|
||||||
|
context.getString(R.string.hide),
|
||||||
|
context.getString(R.string.hide_from_home),
|
||||||
|
tag = "hide",
|
||||||
|
call = { StateMeta.instance.addHiddenVideo(content.url);
|
||||||
if (fragment is HomeFragment) {
|
if (fragment is HomeFragment) {
|
||||||
val removeIndex = recyclerData.results.indexOf(content);
|
val removeIndex = recyclerData.results.indexOf(content);
|
||||||
if (removeIndex >= 0) {
|
if (removeIndex >= 0) {
|
||||||
@@ -124,12 +126,19 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
|
SlideUpMenuItem(context,
|
||||||
{
|
R.drawable.ic_playlist,
|
||||||
|
context.getString(R.string.play_feed_as_queue),
|
||||||
|
context.getString(R.string.play_entire_feed),
|
||||||
|
tag = "playFeed",
|
||||||
|
call = {
|
||||||
val newQueue = listOf(content) + recyclerData.results
|
val newQueue = listOf(content) + recyclerData.results
|
||||||
.filterIsInstance<IPlatformVideo>()
|
.filterIsInstance<IPlatformVideo>()
|
||||||
.filter { it != content };
|
.filter { it != content };
|
||||||
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
|
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue",
|
||||||
|
focus = true,
|
||||||
|
shuffle = false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,21 +156,22 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
adapter.onLongPress.remove(this);
|
adapter.onLongPress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
super.onRestoreCachedData(cachedData)
|
super.onRestoreCachedData(cachedData)
|
||||||
val v = LinearLayout(context).apply {
|
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
orientation = LinearLayout.VERTICAL;
|
|
||||||
};
|
|
||||||
headerView = v;
|
|
||||||
cachedData.adapter.viewsToPrepend.add(v);
|
|
||||||
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
|
override fun createLayoutManager(
|
||||||
val llmResults = LinearLayoutManager(context);
|
recyclerResults: RecyclerView,
|
||||||
llmResults.orientation = LinearLayoutManager.VERTICAL;
|
context: Context
|
||||||
return llmResults;
|
): GridLayoutManager {
|
||||||
|
val glmResults =
|
||||||
|
GridLayoutManager(
|
||||||
|
context,
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
);
|
||||||
|
return glmResults
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollStateChanged(newState: Int) {
|
override fun onScrollStateChanged(newState: Int) {
|
||||||
@@ -183,7 +193,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
|
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
|
||||||
}
|
}
|
||||||
} else if (content is IPlatformPlaylist) {
|
} else if (content is IPlatformPlaylist) {
|
||||||
fragment.navigate<PlaylistFragment>(content);
|
fragment.navigate<RemotePlaylistFragment>(content);
|
||||||
} else if (content is IPlatformPost) {
|
} else if (content is IPlatformPost) {
|
||||||
fragment.navigate<PostDetailFragment>(content);
|
fragment.navigate<PostDetailFragment>(content);
|
||||||
}
|
}
|
||||||
@@ -194,7 +204,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
StatePlayer.instance.clearQueue();
|
StatePlayer.instance.clearQueue();
|
||||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
||||||
};
|
};
|
||||||
ContentType.PLAYLIST -> fragment.navigate<PlaylistFragment>(url);
|
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
|
||||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
||||||
else -> {};
|
else -> {};
|
||||||
}
|
}
|
||||||
@@ -204,11 +214,11 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playPreview() {
|
private fun playPreview() {
|
||||||
if(feedStyle == FeedStyle.THUMBNAIL)
|
if(feedStyle == FeedStyle.THUMBNAIL || recyclerData.layoutManager.spanCount > 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition()
|
||||||
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition();
|
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition()
|
||||||
val itemsVisible = lastVisible - firstVisible + 1;
|
val itemsVisible = lastVisible - firstVisible + 1;
|
||||||
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
||||||
|
|
||||||
@@ -228,7 +238,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopVideo() {
|
private fun stopVideo() {
|
||||||
//TODO: Is this still necessary?
|
//TODO: Is this still necessary?
|
||||||
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
|
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
|
||||||
}
|
}
|
||||||
@@ -256,6 +266,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ContentFeedView";
|
private const val TAG = "ContentFeedView";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user