Compare commits

..

51 Commits

Author SHA1 Message Date
Kelvin 72efb21439 Mark as watched action 2024-07-17 21:12:24 +02:00
Kelvin aa8790ebdb Remove mediasession interval 2024-07-17 20:11:56 +02:00
Kelvin 6d491052ee Invert privatemode boolean 2024-07-17 19:56:38 +02:00
Kelvin 87ff4691ce Merge branch 'playback-experiment' into 'master'
403 Bypass & Privacy mode

See merge request videostreaming/grayjay!26
2024-07-17 17:32:54 +00:00
Kelvin 34d76e79ed Mandatory host body and suffic for wildcard urls 2024-07-17 19:31:59 +02:00
Kelvin 31b43da96f Pass private client pool variable 2024-07-17 18:30:30 +02:00
Kelvin 0540e673e2 Remove under construction on sources 2024-07-17 18:24:59 +02:00
Kelvin 4e88a63809 Privacy mode, Handle 403s 2024-07-17 18:11:08 +02:00
Kelvin f7581f8a65 Block bypass attempts 2024-07-17 13:58:00 +02:00
Kelvin e87a1c079c Experimentation code 2024-07-17 01:37:53 +02:00
Kelvin 3f9477c246 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-16 20:18:54 +02:00
Kelvin 05ed1e188e Logging and refs 2024-07-16 20:18:46 +02:00
Koen f3d06e49f8 Added setting to always proxy requests for FCast. Added logging to print dash manifests. 2024-07-15 10:16:54 +02:00
Koen f9a4b68967 Updated submodules. 2024-07-14 15:39:57 +02:00
Kai DeLorenzo 3631cfe365 fix unstable api error 2024-07-05 12:06:32 -05:00
Koen 8766ae176e Updated Spotify and possible fix for media session being killed after 10 minutes of inactivity. 2024-07-04 10:49:59 +02:00
Kelvin 36b53d490f Exclude HLS and Dash from source dedup 2024-07-03 21:37:15 +02:00
Kelvin f9b8b812a4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-03 20:51:02 +02:00
Kelvin ac9eae5272 refs 2024-07-03 20:50:52 +02:00
Tom f270cc00d8 Merge branch 'github-issue-templates' into 'master'
Add issue template for bugs, docs, feature requests

See merge request videostreaming/grayjay!22
2024-07-02 20:41:49 +00:00
Kelvin a5a3f970da Merge 2024-06-30 18:03:29 +02:00
Kelvin 987c465bf8 Refs 2024-06-30 18:01:41 +02:00
Kai DeLorenzo cf3c766fd9 Update documentation_issue.yml 2024-06-28 15:34:55 +00:00
Kai DeLorenzo 7efafae432 Update feature_request.yml 2024-06-28 15:30:53 +00:00
Kai DeLorenzo 1b8f44dde3 Update bug_report.yml 2024-06-28 15:24:37 +00:00
Thomas Folbrecht 4d93246863 spelling 2024-06-27 14:53:27 -05:00
Thomas Folbrecht 0471886d9f forgot to commit changes to config 2024-06-27 14:31:52 -05:00
Thomas Folbrecht 266974b799 forgot to commit changes to config 2024-06-27 14:30:42 -05:00
Thomas Folbrecht c3663c67d7 add labeler, fix copy 2024-06-27 12:59:03 -05:00
Thomas Folbrecht 07bb23d10b fix license contact link 2024-06-26 20:04:32 -05:00
Thomas Folbrecht 749fc22c6b rm contact 2024-06-26 15:59:46 -05:00
Thomas Folbrecht 9f9a4e8298 Add issue template for bugs, docs, feature requests
points users to chat.futo.org for support
2024-06-26 15:42:12 -05:00
Kai DeLorenzo 39e7d64d3f remove save icon after saving 2024-06-26 15:03:01 -05:00
Kai DeLorenzo 35d8610c00 Update packageHttp.md 2024-06-26 17:01:25 +00:00
Koen bc550ae8f5 Removed exporting service. 2024-06-26 16:01:08 +02:00
Kai DeLorenzo c76ef7f19b Merge branch 'playlist-fixes' into 'master'
Playlist Fixes

See merge request videostreaming/grayjay!21
2024-06-25 15:35:46 +00:00
Kai DeLorenzo b7781264d3 changed playlist limit to 100
added save button to non-saved local playlists
2024-06-25 10:22:23 -05:00
Kai DeLorenzo 696e03941a pass through actions to local playlist and auto convert playlists with 20 or fewer videos 2024-06-24 13:00:58 -05:00
Kai DeLorenzo 4609a351dc don't save playlists that weren't explicitly copied
fixed exception failed to convert playlist job cancelled
2024-06-24 10:50:40 -05:00
Kelvin K c275415a49 Hide playlist video count if unknown 2024-06-20 11:51:11 +02:00
Kelvin K 486ebd6bc8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-19 19:14:20 +02:00
Kelvin K 74b9926647 Refs 2024-06-19 19:14:05 +02:00
Koen 2a6ba6d541 Fixed remote playlist ToPlaylist. 2024-06-14 14:54:37 +02:00
Koen 931216ab7d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:32:10 +02:00
Koen 916936e179 Implemented proper remote playlist support. 2024-06-14 13:32:00 +02:00
Kelvin K b535353365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:23:10 +02:00
Kelvin K be2ae096ee Fix locked content deserializer 2024-06-14 13:22:58 +02:00
Koen 948b85ddcb Pushed updated submodules. 2024-06-14 08:43:18 +02:00
Kelvin K ae904b4cd8 Content recommendation api support, removing old CachedPlatformClient 2024-06-13 17:46:22 +02:00
Kelvin K aad50e7b50 Improved playlist import support 2024-06-13 13:45:31 +02:00
Kelvin K ff28a07871 Fix loop offline videos, make loop not reload video 2024-06-13 11:21:48 +02:00
84 changed files with 1618 additions and 710 deletions
+80
View File
@@ -0,0 +1,80 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["bug", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- All
- Youtube
- BiliBili (CN)
- Twitch
- Odysee
- Rumble
- Kick
- PeerTube
- Patreon
- Nebula
- SoundCloud
- Other
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
+8
View File
@@ -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.
+34
View File
@@ -0,0 +1,34 @@
name: Issue labeler
on:
issues:
types: [ opened ]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
steps:
- uses: actions/checkout@v3
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on plugin field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: plugin
block-list: |
None
Other
token: ${{ secrets.GITHUB_TOKEN }}
-3
View File
@@ -41,9 +41,6 @@
<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" />
+1 -1
View File
@@ -436,7 +436,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;
} }
} }
@@ -525,6 +525,10 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 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)
@@ -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
@@ -491,6 +492,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"));
}
} }
@@ -34,6 +34,7 @@ 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
@@ -710,7 +711,9 @@ class UISlideOverlays {
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, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
{ StatePlayer.instance.addToQueue(video); }), { 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), "watch later", 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",
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }) { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
SlideUpMenuItem(container.context, R.drawable.ic_history, container.context.getString(R.string.add_to_history), "Mark as watched", "history",
{ StateHistory.instance.markAsWatched(video); }),
)); ));
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
@@ -13,6 +13,7 @@ import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@@ -42,7 +42,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.SubscriptionStorage
@@ -79,6 +78,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private lateinit var _fragContainerVideoDetail: FragmentContainerView; private lateinit var _fragContainerVideoDetail: FragmentContainerView;
private lateinit var _fragContainerOverlay: FrameLayout; private lateinit var _fragContainerOverlay: FrameLayout;
//Views
private lateinit var _buttonIncognito: ImageView;
//Frags TopBar //Frags TopBar
lateinit var _fragTopBarGeneral: GeneralTopBarFragment; lateinit var _fragTopBarGeneral: GeneralTopBarFragment;
lateinit var _fragTopBarSearch: SearchTopBarFragment; lateinit var _fragTopBarSearch: SearchTopBarFragment;
@@ -104,6 +106,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainTutorial: TutorialFragment; lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment; lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment; lateinit var _fragMainPlaylist: PlaylistFragment;
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment; lateinit var _fragHistory: HistoryFragment;
lateinit var _fragSourceDetail: SourceDetailFragment; lateinit var _fragSourceDetail: SourceDetailFragment;
@@ -203,6 +206,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
runBlocking { runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity); StatePlatform.instance.updateAvailableClients(this@MainActivity);
} }
@@ -246,6 +250,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources = SourcesFragment.newInstance(); _fragMainSources = SourcesFragment.newInstance();
_fragMainPlaylists = PlaylistsFragment.newInstance(); _fragMainPlaylists = PlaylistsFragment.newInstance();
_fragMainPlaylist = PlaylistFragment.newInstance(); _fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance(); _fragPostDetail = PostDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance(); _fragHistory = HistoryFragment.newInstance();
@@ -288,6 +293,52 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
updateSegmentPaddings(); updateSegmentPaddings();
}; };
_buttonIncognito = findViewById(R.id.incognito_button);
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering?
if(it) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
}
else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
_buttonIncognito.setOnClickListener {
if(!StateApp.instance.privateMode)
return@setOnClickListener;
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
UIDialogs.Action("Cancel", {
StateApp.instance.setPrivacyMode(true);
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Disable", {
StateApp.instance.setPrivacyMode(false);
}, UIDialogs.ActionStyle.DANGEROUS));
};
_fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}");
if(it) {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
else {
if(StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
}
else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
}
StatePlayer.instance.also { StatePlayer.instance.also {
it.onQueueChanged.subscribe { shouldSwapCurrentItem -> it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
if (!shouldSwapCurrentItem) { if (!shouldSwapCurrentItem) {
@@ -331,6 +382,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources.topBar = _fragTopBarAdd; _fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral; _fragMainPlaylists.topBar = _fragTopBarGeneral;
_fragMainPlaylist.topBar = _fragTopBarNavigation; _fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation; _fragPostDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation; _fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation; _fragHistory.topBar = _fragTopBarNavigation;
@@ -535,6 +587,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"IMPORT_OPTIONS" -> { "IMPORT_OPTIONS" -> {
UIDialogs.showImportOptionsDialog(this); UIDialogs.showImportOptionsDialog(this);
} }
"ACTION" -> {
val action = intent.getStringExtra("ACTION");
StateDeveloper.instance.testState = "TestPlayback";
StateDeveloper.instance.testPlayback();
}
"TAB" -> { "TAB" -> {
when(intent.getStringExtra("TAB")){ when(intent.getStringExtra("TAB")){
"Sources" -> { "Sources" -> {
@@ -1044,6 +1101,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
SourcesFragment::class -> _fragMainSources as T; SourcesFragment::class -> _fragMainSources as T;
PlaylistsFragment::class -> _fragMainPlaylists as T; PlaylistsFragment::class -> _fragMainPlaylists as T;
PlaylistFragment::class -> _fragMainPlaylist as T; PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T; PostDetailFragment::class -> _fragPostDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T; WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T; HistoryFragment::class -> _fragHistory as T;
@@ -1176,6 +1234,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return sourcesIntent; return sourcesIntent;
} }
fun getActionIntent(context: Context, action: String) : Intent {
val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "ACTION";
sourcesIntent.putExtra("ACTION", action);
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return sourcesIntent;
}
fun getImportOptionsIntent(context: Context): Intent { fun getImportOptionsIntent(context: Context): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java); val sourcesIntent = Intent(context, MainActivity::class.java);
@@ -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)
@@ -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>?;
} }
@@ -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;
} }
@@ -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 {
@@ -560,7 +563,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 +572,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 +581,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 ")
@@ -5,6 +5,7 @@ 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.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
@@ -77,7 +78,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 && (it[0] != '*' || (_allowRegex.matches(it))) };
return _allowUrlsLowerVal!!; return _allowUrlsLowerVal!!;
}; };
@@ -170,10 +172,12 @@ 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.endsWith(it.substring(1))) };
} }
companion object { companion object {
private val _allowRegex = Regex("\\*\\.[a-z0-9]+\\.[a-z]+");
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig { fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json); val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
if(obj.sourceUrl == null) if(obj.sourceUrl == null)
@@ -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)!!;
} }
} }
@@ -7,7 +7,7 @@ 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
@@ -15,22 +15,26 @@ 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(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
@@ -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);
} }
} }
@@ -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;
@@ -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()
}
} }
@@ -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()
}
} }
@@ -7,6 +7,7 @@ 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 com.futo.platformplayer.BuildConfig
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.ManagedHttpServer import com.futo.platformplayer.api.http.server.ManagedHttpServer
@@ -452,14 +453,22 @@ class StateCasting {
} }
} }
} else { } else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
val url = "http://${ad.localAddress.toString().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 +476,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 {
@@ -667,8 +676,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 +711,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.toString().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,13 +750,28 @@ 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")
@@ -1044,7 +1075,7 @@ 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.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
@@ -1090,8 +1121,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");
@@ -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();
@@ -52,6 +52,7 @@ class PackageBridge : V8Package {
@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);
@@ -16,6 +16,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
@@ -222,6 +223,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
buttons.removeAt(faqIndex) buttons.removeAt(faqIndex)
buttons.add(if (buttons.size == 1) 1 else 0, button) buttons.add(if (buttons.size == 1) 1 else 0, button)
} }
//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 (buttons.size == 2) 2 else 1, button)
}
for (data in buttons) { for (data in buttons) {
val button = MenuButton(context, data, _fragment, true); val button = MenuButton(context, data, _fragment, true);
@@ -305,6 +313,16 @@ class MenuBottomBarFragment : MainActivityFragment() {
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, { 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); it.navigate<BrowserFragment>(Settings.URL_FAQ);
})) }))
newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, {
UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
"All requests will be processed anonymously (unauthenticated), playback and history tracking will 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));
}))
//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
@@ -370,7 +388,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken); c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
} }
}) })
//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
); );
} }
@@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() {
} }
is IPlatformPlaylist -> { is IPlatformPlaylist -> {
fragment.navigate<PlaylistFragment>(v) fragment.navigate<RemotePlaylistFragment>(v)
} }
is IPlatformPost -> { is IPlatformPost -> {
@@ -6,28 +6,32 @@ import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
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
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 {
@@ -183,7 +187,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 +198,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 -> {};
} }
@@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
onSearch.subscribe(this) { onSearch.subscribe(this) {
if(it.isHttpUrl()) { if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it)) if(StatePlatform.instance.hasEnabledPlaylistClient(it))
navigate<PlaylistFragment>(it); navigate<RemotePlaylistFragment>(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it)) else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate<ChannelFragment>(it); navigate<ChannelFragment>(it);
else else
@@ -8,7 +8,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.R
import com.futo.platformplayer.downloads.VideoDownload import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
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.toHumanBytesSize
import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
import com.futo.platformplayer.views.items.ActiveDownloadItem import com.futo.platformplayer.views.items.ActiveDownloadItem
import com.futo.platformplayer.views.items.PlaylistDownloadItem import com.futo.platformplayer.views.items.PlaylistDownloadItem
import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() {
} }
} }
}; };
StateDownloads.instance.onExportsChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
try {
Logger.i(TAG, "Reloading UI for exports");
_view?.reloadUI()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to reload UI for exports", e)
}
}
};
} }
override fun onPause() { override fun onPause() {
@@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() {
StateDownloads.instance.onDownloadsChanged.remove(this); StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this); StateDownloads.instance.onDownloadedChanged.remove(this);
StateDownloads.instance.onExportsChanged.remove(this);
} }
private class DownloadsView : LinearLayout { private class DownloadsView : LinearLayout {
@@ -12,16 +12,21 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
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.adapters.viewholders.ImportPlaylistsViewHolder import com.futo.platformplayer.views.adapters.viewholders.ImportPlaylistsViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportPlaylistsFragment : MainFragment() { class ImportPlaylistsFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView : Boolean = true;
@@ -67,7 +72,7 @@ class ImportPlaylistsFragment : MainFragment() {
private val _items: ArrayList<SelectablePlaylist> = arrayListOf(); private val _items: ArrayList<SelectablePlaylist> = arrayListOf();
private var _currentLoadIndex = 0; private var _currentLoadIndex = 0;
private var _taskLoadPlaylist: TaskHandler<String, Playlist?>; private var _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails?>;
constructor(fragment: ImportPlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) { constructor(fragment: ImportPlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment; _fragment = fragment;
@@ -102,7 +107,7 @@ class ImportPlaylistsFragment : MainFragment() {
setLoading(false); setLoading(false);
_taskLoadPlaylist = TaskHandler<String, Playlist?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link).toPlaylist(); }) _taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); })
.success { .success {
if (it != null) { if (it != null) {
_items.add(SelectablePlaylist(it)); _items.add(SelectablePlaylist(it));
@@ -113,7 +118,7 @@ class ImportPlaylistsFragment : MainFragment() {
}.exceptionWithParameter<Throwable> { ex, para -> }.exceptionWithParameter<Throwable> { ex, para ->
//setLoading(false); //setLoading(false);
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex); Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false) UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false)
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); }); //UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
loadNext(); loadNext();
}; };
@@ -147,12 +152,32 @@ class ImportPlaylistsFragment : MainFragment() {
it.title = context.getString(R.string.import_playlists); it.title = context.getString(R.string.import_playlists);
it.onImport.subscribe(this) { it.onImport.subscribe(this) {
val playlistsToImport = _items.filter { i -> i.selected }.toList(); val playlistsToImport = _items.filter { i -> i.selected }.toList();
for (playlistToImport in playlistsToImport) {
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
}
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported)); UIDialogs.showDialogProgress(context) {
_fragment.closeSegment(); it.setText("Importing playlists..");
it.setProgress(0f);
_fragment.lifecycleScope.launch(Dispatchers.IO) {
for ((i, playlistToImport) in playlistsToImport.withIndex()) {
withContext(Dispatchers.Main) {
it.setText("Importing playlists..\n[${playlistToImport.playlist.name}]");
}
try {
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist.toPlaylist());
}
catch(ex: Throwable) {
UIDialogs.appToast("Failed to import [${playlistToImport.playlist.name}]\n" + ex.message);
}
withContext(Dispatchers.Main) {
it.setProgress(i.toDouble() / playlistsToImport.size);
}
}
withContext(Dispatchers.Main) {
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
_fragment.closeSegment();
it.dismiss();
}
}
}
}; };
} }
} }
@@ -1,14 +1,11 @@
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.drawable.Animatable
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
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.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
@@ -30,7 +27,6 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PlaylistFragment : MainFragment() { class PlaylistFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView : Boolean = true;
@@ -70,7 +66,6 @@ class PlaylistFragment : MainFragment() {
private val _fragment: PlaylistFragment; private val _fragment: PlaylistFragment;
private var _playlist: Playlist? = null; private var _playlist: Playlist? = null;
private var _remotePlaylist: IPlatformPlaylistDetails? = null;
private var _editPlaylistNameInput: SlideUpMenuTextInput? = null; private var _editPlaylistNameInput: SlideUpMenuTextInput? = null;
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null; private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
private var _url: String? = null; private var _url: String? = null;
@@ -136,12 +131,11 @@ class PlaylistFragment : MainFragment() {
return@TaskHandler StatePlatform.instance.getPlaylist(it); return@TaskHandler StatePlatform.instance.getPlaylist(it);
}) })
.success { .success {
setLoading(false);
_remotePlaylist = it;
setName(it.name); setName(it.name);
setVideos(it.contents.getResults(), false);
setVideoCount(it.videoCount);
//TODO: Implement support for pagination //TODO: Implement support for pagination
setVideos(it.toPlaylist().videos, false);
setVideoCount(it.videoCount);
setLoading(false);
} }
.exception<Throwable> { .exception<Throwable> {
Logger.w(TAG, "Failed to load playlist.", it); Logger.w(TAG, "Failed to load playlist.", it);
@@ -151,58 +145,62 @@ class PlaylistFragment : MainFragment() {
} }
fun onShown(parameter: Any?) { fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel(); _taskLoadPlaylist.cancel()
if (parameter is Playlist?) { if (parameter is Playlist?) {
_playlist = parameter; _playlist = parameter
_remotePlaylist = null; _url = null
_url = null;
if(parameter != null) { if (parameter != null) {
setName(parameter.name); setName(parameter.name)
setVideos(parameter.videos, true); setVideos(parameter.videos, true)
setVideoCount(parameter.videos.size); setVideoCount(parameter.videos.size)
setButtonDownloadVisible(true); setButtonDownloadVisible(true)
setButtonEditVisible(true); setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
_fragment.topBar?.assume<NavigationTopBarFragment>()
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
StatePlaylists.instance.playlistStore.save(parameter)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
}))
}
} else { } else {
setName(null); setName(null)
setVideos(null, false); setVideos(null, false)
setVideoCount(-1); setVideoCount(-1)
setButtonDownloadVisible(false); setButtonDownloadVisible(false)
setButtonEditVisible(false); setButtonEditVisible(false)
} }
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
} else if (parameter is IPlatformPlaylist) { } else if (parameter is IPlatformPlaylist) {
_playlist = null; _playlist = null
_remotePlaylist = null; _url = parameter.url
_url = parameter.url;
setVideoCount(parameter.videoCount); setVideoCount(parameter.videoCount)
setName(parameter.name); setName(parameter.name)
setVideos(null, false); setVideos(null, false)
setButtonDownloadVisible(false); setButtonDownloadVisible(false)
setButtonEditVisible(false); setButtonEditVisible(false)
fetchPlaylist(); fetchPlaylist()
showConvertPlaylistButton();
} else if (parameter is String) { } else if (parameter is String) {
_playlist = null; _playlist = null
_remotePlaylist = null; _url = parameter
_url = parameter;
setName(null); setName(null)
setVideos(null, false); setVideos(null, false)
setVideoCount(-1); setVideoCount(-1)
setButtonDownloadVisible(false); setButtonDownloadVisible(false)
setButtonEditVisible(false); setButtonEditVisible(false)
fetchPlaylist(); fetchPlaylist()
showConvertPlaylistButton();
} }
_playlist?.let { _playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download); updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download)
} }
} }
@@ -242,34 +240,6 @@ class PlaylistFragment : MainFragment() {
StateDownloads.instance.onDownloadedChanged.remove(this); StateDownloads.instance.onDownloadedChanged.remove(this);
} }
private fun showConvertPlaylistButton() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
val remotePlaylist = _remotePlaylist;
if (remotePlaylist == null) {
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
return@Pair;
}
setLoading(true);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
StatePlaylists.instance.playlistStore.save(remotePlaylist.toPlaylist());
withContext(Dispatchers.Main) {
setLoading(false);
UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist));
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
setLoading(false);
}
throw e;
}
}
}));
}
private fun fetchPlaylist() { private fun fetchPlaylist() {
Logger.i(TAG, "fetchPlaylist") Logger.i(TAG, "fetchPlaylist")
@@ -290,21 +260,15 @@ class PlaylistFragment : MainFragment() {
override fun onPlayAllClick() { override fun onPlayAllClick() {
val playlist = _playlist; val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) { if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true); StatePlayer.instance.setPlaylist(playlist, focus = true);
} else if (remotePlaylist != null) {
StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = false);
} }
} }
override fun onShuffleClick() { override fun onShuffleClick() {
val playlist = _playlist; val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) { if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true); StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
} else if (remotePlaylist != null) {
StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = true);
} }
} }
@@ -320,19 +284,12 @@ class PlaylistFragment : MainFragment() {
} }
override fun onVideoClicked(video: IPlatformVideo) { override fun onVideoClicked(video: IPlatformVideo) {
val playlist = _playlist; val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) { if (playlist != null) {
val index = playlist.videos.indexOf(video); val index = playlist.videos.indexOf(video);
if (index == -1) if (index == -1)
return; return;
StatePlayer.instance.setPlaylist(playlist, index, true); StatePlayer.instance.setPlaylist(playlist, index, true);
} else if (remotePlaylist != null) {
val index = remotePlaylist.contents.getResults().indexOf(video);
if (index == -1)
return;
StatePlayer.instance.setPlaylist(remotePlaylist, index, true);
} }
} }
} }
@@ -0,0 +1,408 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.app.ShareCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiPager
import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.VideoListEditorViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
enum class Action {
PLAY_ALL, SHUFFLE, PLAY, NONE
}
class RemotePlaylistFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _view: RemotePlaylistView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
_view?.onShown(parameter);
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = RemotePlaylistView(this, inflater);
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
}
@SuppressLint("ViewConstructor")
class RemotePlaylistView : LinearLayout {
private val _fragment: RemotePlaylistFragment;
private var _remotePlaylist: IPlatformPlaylistDetails? = null;
private var _remotePlaylistPagerWindow: IPager<IPlatformVideo>? = null;
private var _url: String? = null;
private val _videos: ArrayList<IPlatformVideo> = arrayListOf();
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
private var _nextPageHandler: TaskHandler<IPager<IPlatformVideo>, List<IPlatformVideo>>;
private var _imagePlaylistThumbnail: ImageView;
private var _textName: TextView;
private var _textMetadata: TextView;
private var _loaderOverlay: FrameLayout;
private var _imageLoader: ImageView;
private var _overlayContainer: FrameLayout;
private var _buttonShare: ImageButton;
private var _recyclerPlaylist: RecyclerView;
private var _llmPlaylist: LinearLayoutManager;
private val _adapterVideos: InsertedViewAdapterWithLoader<VideoListEditorViewHolder>;
private val _scrollListener: RecyclerView.OnScrollListener
constructor(fragment: RemotePlaylistFragment, inflater: LayoutInflater) : super(inflater.context) {
inflater.inflate(R.layout.fragment_remote_playlist, this);
_fragment = fragment;
_textName = findViewById(R.id.text_name);
_textMetadata = findViewById(R.id.text_metadata);
_imagePlaylistThumbnail = findViewById(R.id.image_playlist_thumbnail);
_loaderOverlay = findViewById(R.id.layout_loading_overlay);
_imageLoader = findViewById(R.id.image_loader);
_recyclerPlaylist = findViewById(R.id.recycler_playlist);
_llmPlaylist = LinearLayoutManager(context);
_adapterVideos = InsertedViewAdapterWithLoader(context,
arrayListOf(),
arrayListOf(),
childCountGetter = { _videos.size },
childViewHolderBinder = { viewHolder, position ->
viewHolder.bind(
_videos[position],
false
)
},
childViewHolderFactory = { viewGroup, _ ->
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.list_playlist, viewGroup, false)
val holder = VideoListEditorViewHolder(view, null)
holder.onClick.subscribe {
convertPlaylist(false, Action.PLAY, holder.video)
}
return@InsertedViewAdapterWithLoader holder
})
_recyclerPlaylist.adapter = _adapterVideos;
_recyclerPlaylist.layoutManager = _llmPlaylist;
_overlayContainer = findViewById(R.id.overlay_container);
val buttonPlayAll = findViewById<LinearLayout>(R.id.button_play_all);
val buttonShuffle = findViewById<LinearLayout>(R.id.button_shuffle);
_buttonShare = findViewById(R.id.button_share);
_buttonShare.setOnClickListener {
val remotePlaylist = _remotePlaylist ?: return@setOnClickListener;
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(remotePlaylist.shareUrl)
.intent);
};
buttonPlayAll.setOnClickListener {
convertPlaylist(false, Action.PLAY_ALL);
};
buttonShuffle.setOnClickListener {
convertPlaylist(false, Action.SHUFFLE);
};
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
StateApp.instance.scopeGetter,
{
return@TaskHandler StatePlatform.instance.getPlaylist(it);
})
.success {
_remotePlaylist = it;
val c = it.contents;
_remotePlaylistPagerWindow = if (c is ReusablePager) c.getWindow() else c;
setName(it.name);
setVideos(_remotePlaylistPagerWindow!!.getResults());
setVideoCount(it.videoCount);
setLoading(false);
}
.exception<Throwable> {
Logger.w(TAG, "Failed to load playlist.", it);
val c = context ?: return@exception;
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
};
_nextPageHandler = TaskHandler<IPager<IPlatformVideo>, List<IPlatformVideo>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>)
it.nextPageAsync();
else
it.nextPage();
processPagerExceptions(it);
return@TaskHandler it.getResults();
}).success {
_adapterVideos.setLoading(false);
addVideos(it);
//TODO: ensureEnoughContentVisible()
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
loadNextPage();
}, null, fragment);
};
_scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = _recyclerPlaylist.childCount
val firstVisibleItem = _llmPlaylist.findFirstVisibleItemPosition()
val visibleThreshold = 15
if (!_adapterVideos.isLoading && firstVisibleItem + visibleItemCount + visibleThreshold >= _videos.size) {
loadNextPage()
}
}
}
_recyclerPlaylist.addOnScrollListener(_scrollListener)
}
private fun loadNextPage() {
val pager: IPager<IPlatformVideo> = _remotePlaylistPagerWindow ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
if (pager.hasMorePages()) {
_adapterVideos.setLoading(true);
_nextPageHandler.run(pager);
}
}
private fun processPagerExceptions(pager: IPager<*>) {
if(pager is MultiPager<*> && pager.allowFailure) {
val ex = pager.getResultExceptions();
for(kv in ex) {
val jsVideoPager: JSPager<*>? = if(kv.key is MultiPager<*>)
(kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?;
else if(kv.key is JSPager<*>)
kv.key as JSPager<*>;
else null;
context?.let {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
if(jsVideoPager != null)
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", jsVideoPager.getPluginConfig().name).replace("{message}", kv.value.message ?: ""), false);
else
UIDialogs.toast(it, kv.value.message ?: "", false);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e)
}
}
}
}
}
}
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel();
_nextPageHandler.cancel();
if (parameter is IPlatformPlaylist) {
_remotePlaylist = null;
_url = parameter.url;
setVideoCount(parameter.videoCount);
setName(parameter.name);
setVideos(null);
fetchPlaylist();
showConvertPlaylistButton();
} else if (parameter is String) {
_remotePlaylist = null;
_url = parameter;
setName(null);
setVideos(null);
setVideoCount(-1);
fetchPlaylist();
showConvertPlaylistButton();
}
}
private fun convertPlaylist(
savePlaylist: Boolean, action: Action, video: IPlatformVideo? = null
) {
val remotePlaylist = _remotePlaylist
if (remotePlaylist == null) {
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading))
return
}
val convert = {
setLoading(true)
UIDialogs.showDialogProgress(context) {
it.setText("Converting playlist..")
it.setProgress(0f)
_fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val playlist = remotePlaylist.toPlaylist { progress ->
_fragment.lifecycleScope.launch(Dispatchers.Main) {
it.setProgress(progress.toDouble() / remotePlaylist.videoCount)
}
}
if (savePlaylist) {
StatePlaylists.instance.playlistStore.save(playlist)
}
_fragment.lifecycleScope.launch(Dispatchers.Main) {
UIDialogs.toast("Playlist converted")
it.dismiss()
_fragment.navigate<PlaylistFragment>(playlist)
when (action) {
Action.SHUFFLE -> StatePlayer.instance.setPlaylist(
playlist, focus = true, shuffle = true
)
Action.PLAY_ALL -> StatePlayer.instance.setPlaylist(
playlist, focus = true
)
Action.PLAY -> {
StatePlayer.instance.setPlaylist(
playlist, _videos.indexOf(video), true
)
}
Action.NONE -> {}
}
}
} catch (ex: Throwable) {
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message)
}
}
}
}
if (remotePlaylist.videoCount > 100) {
val c = context ?: return
UIDialogs.showConfirmationDialog(
c, "Conversion to local playlist is required for this action", convert
)
} else {
convert()
}
}
private fun showConvertPlaylistButton() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
convertPlaylist(true, Action.NONE);
}));
}
private fun fetchPlaylist() {
Logger.i(TAG, "fetchPlaylist")
val url = _url;
if (!url.isNullOrBlank()) {
setLoading(true);
_taskLoadPlaylist.run(url);
}
}
private fun setName(name: String?) {
_textName.text = name ?: "";
}
private fun setVideoCount(videoCount: Int = -1) {
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
}
private fun setVideos(videos: List<IPlatformVideo>?) {
if (!videos.isNullOrEmpty()) {
val video = videos.first();
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
};
} else {
_textMetadata.text = "0 " + context.getString(R.string.videos);
Glide.with(_imagePlaylistThumbnail)
.load(R.drawable.placeholder_video_thumbnail)
.into(_imagePlaylistThumbnail)
}
synchronized(_videos) {
_videos.clear();
_videos.addAll(videos ?: listOf());
_adapterVideos.notifyDataSetChanged();
}
}
private fun addVideos(videos: List<IPlatformVideo>) {
synchronized(_videos) {
val index = _videos.size;
_videos.addAll(videos);
_adapterVideos.notifyItemRangeInserted(_adapterVideos.childToParentPosition(index), videos.size);
}
}
private fun setLoading(isLoading: Boolean) {
if (isLoading){
(_imageLoader.drawable as Animatable?)?.start()
_loaderOverlay.visibility = View.VISIBLE;
}
else {
_loaderOverlay.visibility = View.GONE;
(_imageLoader.drawable as Animatable?)?.stop()
}
}
}
companion object {
private const val TAG = "RemotePlaylistFragment";
fun newInstance() = RemotePlaylistFragment().apply {}
}
}
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.models.Thumbnail
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.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@@ -144,10 +145,8 @@ class TutorialFragment : MainFragment() {
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> { override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
return EmptyPager() return EmptyPager()
} }
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getPlaybackTracker(): IPlaybackTracker? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
return null
}
} }
companion object { companion object {
@@ -39,6 +39,7 @@ class VideoDetailFragment : MainFragment {
private var _view : SingleViewTouchableMotionLayout? = null; private var _view : SingleViewTouchableMotionLayout? = null;
var isFullscreen : Boolean = false; var isFullscreen : Boolean = false;
val onFullscreenChanged = Event1<Boolean>();
var isTransitioning : Boolean = false var isTransitioning : Boolean = false
private set; private set;
var isInPictureInPicture : Boolean = false var isInPictureInPicture : Boolean = false
@@ -424,6 +425,7 @@ class VideoDetailFragment : MainFragment {
changeOrientation(OrientationManager.Orientation.PORTRAIT); changeOrientation(OrientationManager.Orientation.PORTRAIT);
} }
isFullscreen = fullscreen; isFullscreen = fullscreen;
onFullscreenChanged.emit(isFullscreen);
_view?.allowMotion = !fullscreen; _view?.allowMotion = !fullscreen;
} }
private fun changeOrientation(orientation: OrientationManager.Orientation) { private fun changeOrientation(orientation: OrientationManager.Orientation) {
@@ -59,8 +59,11 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
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.DashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.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.LocalAudioSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
@@ -99,6 +102,7 @@ import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@@ -158,6 +162,7 @@ import java.time.OffsetDateTime
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong import kotlin.math.roundToLong
@androidx.media3.common.util.UnstableApi
class VideoDetailView : ConstraintLayout { class VideoDetailView : ConstraintLayout {
private val TAG = "VideoDetailView" private val TAG = "VideoDetailView"
@@ -1063,6 +1068,11 @@ class VideoDetailView : ConstraintLayout {
if(!bypassSameVideoCheck && this.video?.url == video.url) if(!bypassSameVideoCheck && this.video?.url == video.url)
return; return;
//Loop workaround
if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
_player.seekTo(0);
return;
}
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id); val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
if(cachedVideo != null) { if(cachedVideo != null) {
@@ -1161,6 +1171,8 @@ class VideoDetailView : ConstraintLayout {
//@OptIn(ExperimentalCoroutinesApi::class) //@OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})") Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
_didTriggerDatasourceError = false;
if(newVideo && this.video?.url == videoDetail.url) if(newVideo && this.video?.url == videoDetail.url)
return; return;
@@ -1227,18 +1239,25 @@ class VideoDetailView : ConstraintLayout {
}*/ }*/
} }
try { try {
val stopwatch = com.futo.platformplayer.debug.Stopwatch() if(!StateApp.instance.privateMode) {
var tracker = video.getPlaybackTracker() val stopwatch = com.futo.platformplayer.debug.Stopwatch()
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms") var tracker = video.getPlaybackTracker()
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
if (tracker == null) { if (tracker == null) {
stopwatch.reset() stopwatch.reset()
tracker = StatePlatform.instance.getPlaybackTracker(video.url); tracker = StatePlatform.instance.getPlaybackTracker(video.url);
Logger.i(TAG, "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms") Logger.i(
TAG,
"StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
)
}
if (me.video == video)
me._playbackTracker = tracker;
} }
else if(me.video == video)
if(me.video == video) me._playbackTracker = null;
me._playbackTracker = tracker;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Playback tracker failed", ex); Logger.e(TAG, "Playback tracker failed", ex);
@@ -1442,6 +1461,8 @@ class VideoDetailView : ConstraintLayout {
StatePlayer.instance.startOrUpdateMediaSession(context, video); StatePlayer.instance.startOrUpdateMediaSession(context, video);
StatePlayer.instance.setCurrentlyPlaying(video); StatePlayer.instance.setCurrentlyPlaying(video);
_liveChat?.stop();
_liveChat = null;
if(video.isLive && video.live != null) { if(video.isLive && video.live != null) {
loadLiveChat(video); loadLiveChat(video);
} }
@@ -1638,6 +1659,7 @@ class VideoDetailView : ConstraintLayout {
} }
} }
private var _didTriggerDatasourceErrroCount = 0;
private var _didTriggerDatasourceError = false; private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) { private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception); Logger.e(TAG, "onDataSourceError", exception);
@@ -1647,26 +1669,49 @@ class VideoDetailView : ConstraintLayout {
return; return;
val config = currentVideo.sourceConfig; val config = currentVideo.sourceConfig;
if(!_didTriggerDatasourceError) { if(_didTriggerDatasourceErrroCount <= 3) {
_didTriggerDatasourceError = true; _didTriggerDatasourceError = true;
_didTriggerDatasourceErrroCount++;
UIDialogs.toast("Block detected, attempting bypass");
fragment.lifecycleScope.launch(Dispatchers.IO) {
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
val previousVideoSource = _lastVideoSource;
val previousAudioSource = _lastAudioSource;
if(newDetails is IPlatformVideoDetails) {
val newVideoSource = if(previousVideoSource != null)
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
else null;
val newAudioSource = if(previousAudioSource != null)
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
else null;
withContext(Dispatchers.Main) {
video = newDetails;
_player.setSource(newVideoSource, newAudioSource, true, true);
}
}
}
}
else if(_didTriggerDatasourceErrroCount > 3) {
UIDialogs.showDialog(context, R.drawable.ic_error_pred, UIDialogs.showDialog(context, R.drawable.ic_error_pred,
context.getString(R.string.media_error), context.getString(R.string.media_error),
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental), context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
null, null,
0, 0,
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }), UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
UIDialogs.Action(context.getString(R.string.yes), { UIDialogs.Action(context.getString(R.string.yes), {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
StatePlatform.instance.reloadClient(context, config.id); StatePlatform.instance.reloadClient(context, config.id);
reloadVideo(); reloadVideo();
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to reload video.", e) Logger.e(TAG, "Failed to reload video.", e)
}
} }
}, UIDialogs.ActionStyle.PRIMARY) }
); }, UIDialogs.ActionStyle.PRIMARY)
);
} }
} }
} }
@@ -1763,15 +1808,21 @@ class VideoDetailView : ConstraintLayout {
} }
} }
val bestVideoSources = videoSources?.map { it.height * it.width } val doDedup = false;
val bestVideoSources = if(doDedup) (videoSources?.map { it.height * it.width }
?.distinct() ?.distinct()
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } ?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))
?.distinct()
?.filter { it != null } ?.filter { it != null }
?.toList() ?: listOf(); ?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }; val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
val bestAudioSources = audioSources val bestAudioSources = if(doDedup) audioSources
?.filter { it.container == bestAudioContainer } ?.filter { it.container == bestAudioContainer }
?.toList() ?: listOf(); ?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
?.distinct()
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
@@ -2299,6 +2350,15 @@ class VideoDetailView : ConstraintLayout {
} }
updateTracker(positionMilliseconds, isPlaying, false); updateTracker(positionMilliseconds, isPlaying, false);
if(StateDeveloper.instance.isPlaybackTesting) {
if((positionMilliseconds > 1000 * 65 || positionMilliseconds > (video!!.duration * 1000 - 1000))) {
StateDeveloper.instance.testPlayback();
}
else if(video!!.duration > 70 && positionMilliseconds < 10000) {
handleSeek(55000);
}
}
} }
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) { private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
@@ -127,7 +127,7 @@ class VideoHelper {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource { fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
val urlToUse = videoSource.getVideoUrl(); val urlToUse = videoSource.getVideoUrl();
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse, val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
videoSource.duration * 1000, videoSource.duration * 1000,
@@ -145,10 +145,10 @@ class VideoHelper {
); );
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream()); val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec -> return Pair(DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null); Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
return@Resolver dataSpec; return@Resolver dataSpec;
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()) })).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()), manifestConfig);
} }
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata { fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
@@ -2,6 +2,7 @@ package com.futo.platformplayer.serializers
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformLockedContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
@@ -30,6 +31,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPla
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer(); "NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented"); "ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
"POST" -> SerializedPlatformPost.serializer(); "POST" -> SerializedPlatformPost.serializer();
"LOCKED" -> SerializedPlatformLockedContent.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}") else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
}; };
} else { } else {
@@ -38,6 +40,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPla
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer(); ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented"); ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
ContentType.POST.value -> SerializedPlatformPost.serializer(); ContentType.POST.value -> SerializedPlatformPost.serializer();
ContentType.LOCKED.value -> SerializedPlatformLockedContent.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj.jsonPrimitive.int}") else -> throw NotImplementedError("Unknown Content Type Value: ${obj.jsonPrimitive.int}")
}; };
} }
@@ -1,236 +0,0 @@
package com.futo.platformplayer.services
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.downloads.VideoExport
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.share
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.stores.FragmentedStorage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime
import java.util.UUID
class ExportingService : Service() {
private val TAG = "ExportingService";
private val EXPORT_NOTIF_ID = 4;
private val EXPORT_NOTIF_TAG = "export";
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
//Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
private var _notificationManager: NotificationManager? = null;
private var _notificationChannel: NotificationChannel? = null;
private val _client = ManagedHttpClient();
private var _started = false;
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.i(TAG, "onStartCommand");
synchronized(this) {
if(_started)
return START_STICKY;
if(!FragmentedStorage.isInitialized) {
closeExportSession();
return START_NOT_STICKY;
}
_started = true;
}
setupNotificationRequirements();
_callOnStarted?.invoke(this);
_instance = this;
_scope.launch {
try {
doExporting();
}
catch(ex: Throwable) {
try {
StateAnnouncement.instance.registerAnnouncementSession(
Announcement(
"rootExportException",
"An root export service exception happened",
ex.message ?: "",
AnnouncementType.SESSION,
OffsetDateTime.now()
)
);
} catch(_: Throwable){}
}
};
return START_STICKY;
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
}
override fun onCreate() {
Logger.i(TAG, "onCreate");
super.onCreate()
}
override fun onBind(p0: Intent?): IBinder? {
return null;
}
private suspend fun doExporting() {
Logger.i(TAG, "doExporting - Starting Exports");
val ignore = mutableListOf<VideoExport>();
var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull();
while (currentExport != null)
{
try{
notifyExport(currentExport);
doExport(applicationContext, currentExport);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
currentExport.error = ex.message;
currentExport.changeState(VideoExport.State.ERROR);
ignore.add(currentExport);
//Give it a sec
Thread.sleep(500);
}
currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull();
}
Logger.i(TAG, "doExporting - Ending Exports");
stopService(this);
}
private suspend fun doExport(context: Context, export: VideoExport) {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
export.changeState(VideoExport.State.EXPORTING);
var lastNotifyTime: Long = 0L;
val file = export.export(context) { progress ->
export.progress = progress;
val currentTime = System.currentTimeMillis();
if (currentTime - lastNotifyTime > 500) {
notifyExport(export);
lastNotifyTime = currentTime;
}
}
export.changeState(VideoExport.State.COMPLETED);
Logger.i(TAG, "Export [${export.videoLocal.name}] finished");
StateDownloads.instance.removeExport(export);
notifyExport(export);
withContext(Dispatchers.Main) {
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(this@ExportingService);
};
}
}
private fun notifyExport(export: VideoExport) {
val channel = _notificationChannel ?: return;
val bringUpIntent = Intent(this, MainActivity::class.java);
bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
bringUpIntent.action = "TAB";
bringUpIntent.putExtra("TAB", "Exports");
var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG)
.setSmallIcon(R.drawable.ic_export)
.setOngoing(true)
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setContentTitle("${export.state}: ${export.videoLocal.name}")
.setContentText(export.getExportInfo())
.setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0)
.setChannelId(channel.id)
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
} else {
startForeground(EXPORT_NOTIF_ID, notif);
}
}
fun closeExportSession() {
Logger.i(TAG, "closeExportSession");
stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(EXPORT_NOTIF_ID);
stopService();
_started = false;
super.stopSelf();
}
override fun onDestroy() {
Logger.i(TAG, "onDestroy");
_instance = null;
_scope.cancel("onDestroy");
super.onDestroy();
}
companion object {
private var _instance: ExportingService? = null;
private var _callOnStarted: ((ExportingService)->Unit)? = null;
@Synchronized
fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) {
if(!FragmentedStorage.isInitialized)
return;
if(_instance == null) {
_callOnStarted = handle;
val intent = Intent(context, ExportingService::class.java);
context.startForegroundService(intent);
}
else _instance?.let {
if(handle != null)
handle(it);
}
}
@Synchronized
fun getService() : ExportingService? {
return _instance;
}
@Synchronized
fun stopService(service: ExportingService? = null) {
(service ?: _instance)?.let {
if(_instance == it)
_instance = null;
it.closeExportSession();
}
}
}
}
@@ -15,7 +15,9 @@ import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener import android.media.AudioManager.OnAudioFocusChangeListener
import android.media.MediaMetadata import android.media.MediaMetadata
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.os.SystemClock import android.os.SystemClock
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
@@ -56,6 +57,18 @@ class StateApp {
val sessionId = UUID.randomUUID().toString(); val sessionId = UUID.randomUUID().toString();
var privateMode: Boolean = false
get(){
return field;
}
private set(value) {
field = value;
}
val privateModeChanged = Event1<Boolean>();
fun setPrivacyMode(value: Boolean) {
privateMode = value;
privateModeChanged.emit(privateMode);
}
fun getExternalGeneralDirectory(context: Context): DocumentFile? { fun getExternalGeneralDirectory(context: Context): DocumentFile? {
val generalUri = Settings.instance.storage.getStorageGeneralUri(); val generalUri = Settings.instance.storage.getStorageGeneralUri();
@@ -445,9 +458,6 @@ class StateApp {
DownloadService.getOrCreateService(context); DownloadService.getOrCreateService(context);
} }
Logger.i(TAG, "MainApp Started: Check [Exports]");
StateDownloads.instance.checkForExportTodos();
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]"); Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled(); val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
val shouldDownload = Settings.instance.autoUpdate.shouldDownload(); val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
@@ -1,11 +1,19 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.server.ManagedHttpServer import com.futo.platformplayer.api.http.server.ManagedHttpServer
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.developer.DeveloperEndpoints import com.futo.platformplayer.developer.DeveloperEndpoints
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
/*** /***
@@ -23,6 +31,12 @@ class StateDeveloper {
var devProxy: DevProxySettings? = null; var devProxy: DevProxySettings? = null;
var testState: String? = null;
val isPlaybackTesting: Boolean get() {
return SettingsDev.instance.developerMode && testState == "TestPlayback";
};
fun initializeDev(id: String) { fun initializeDev(id: String) {
currentDevID = id; currentDevID = id;
synchronized(_devLogs) { synchronized(_devLogs) {
@@ -135,6 +149,37 @@ class StateDeveloper {
} }
private var homePager: IPager<IPlatformContent>? = null;
private var pagerIndex = 0;
fun testPlayback(){
val mainActivity = if(StateApp.instance.isMainActive) StateApp.instance.context as MainActivity else return;
StateApp.instance.scope.launch(Dispatchers.IO) {
if(homePager == null)
homePager = StatePlatform.instance.getHome();
var pager = homePager ?: return@launch;
pagerIndex++;
val video = if(pager.getResults().size <= pagerIndex) {
if(!pager.hasMorePages()) {
homePager = StatePlatform.instance.getHome();
pager = homePager as IPager<IPlatformContent>;
}
pager.nextPage();
pagerIndex = 0;
val results = pager.getResults();
if(results.size <= 0)
null;
else
results[0];
}
else
pager.getResults()[pagerIndex];
StateApp.instance.scope.launch(Dispatchers.Main) {
mainActivity.navigate(mainActivity._fragVideoDetail, video);
}
}
}
companion object { companion object {
const val DEV_ID = "DEV"; const val DEV_ID = "DEV";
@@ -152,6 +197,7 @@ class StateDeveloper {
it._server?.stop(); it._server?.stop();
} }
} }
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@@ -1,13 +1,13 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.os.StatFs import android.os.StatFs
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
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.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
@@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.PlaylistDownloaded import com.futo.platformplayer.models.PlaylistDownloaded
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.services.ExportingService import com.futo.platformplayer.share
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.UUID
/*** /***
* Used to maintain downloads * Used to maintain downloads
@@ -50,12 +54,8 @@ class StateDownloads {
private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads") private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads")
.load(); .load();
private val _exporting = FragmentedStorage.storeJson<VideoExport>("exporting")
.load();
private lateinit var _downloadedSet: HashSet<PlatformID>; private lateinit var _downloadedSet: HashSet<PlatformID>;
val onExportsChanged = Event0();
val onDownloadsChanged = Event0(); val onDownloadsChanged = Event0();
val onDownloadedChanged = Event0(); val onDownloadedChanged = Event0();
@@ -457,17 +457,6 @@ class StateDownloads {
} }
} }
try {
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
for (export in exporting)
_exporting.delete(export);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to delete dangling export:", ex);
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
}
return Pair(totalDeletedCount, totalDeleted); return Pair(totalDeletedCount, totalDeleted);
} }
@@ -475,66 +464,41 @@ class StateDownloads {
return _downloadsDirectory; return _downloadsDirectory;
} }
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
var lastNotifyTime = -1L;
UIDialogs.showDialogProgress(context) {
it.setText("Exporting content..");
it.setProgress(0f);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
try {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
//Export val file = export.export(context) { progress ->
fun getExporting(): List<VideoExport> { val now = System.currentTimeMillis();
return _exporting.getItems(); if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
} it.setProgress(progress);
fun checkForExportTodos() { lastNotifyTime = now;
if(_exporting.hasItems()) { }
StateApp.withContext { }
ExportingService.getOrCreateService(it);
withContext(Dispatchers.Main) {
it.setProgress(100.0f)
it.dismiss()
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(context);
};
}
} catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex);
}
} }
} }
} }
fun validateExport(export: VideoExport) {
if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url })
throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export");
}
fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) {
val shortName = if(videoLocal.name.length > 23)
videoLocal.name.substring(0, 20) + "...";
else
videoLocal.name;
val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
try {
validateExport(videoExport);
_exporting.save(videoExport);
if(notify) {
UIDialogs.toast("Exporting [${shortName}]");
StateApp.withContext { ExportingService.getOrCreateService(it) };
onExportsChanged.emit();
}
}
catch (ex: AlreadyQueuedException) {
Logger.e(TAG, "File is already queued for export.", ex);
StateApp.withContext { ExportingService.getOrCreateService(it) };
}
catch(ex: Throwable) {
StateApp.withContext {
UIDialogs.showDialog(
it,
R.drawable.ic_error,
"Failed to start export due to:\n${ex.message}", null, null,
0,
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)
);
}
}
}
fun removeExport(export: VideoExport) {
_exporting.delete(export);
export.isCancelled = true;
onExportsChanged.emit();
}
companion object { companion object {
const val TAG = "StateDownloads"; const val TAG = "StateDownloads";
@@ -96,6 +96,8 @@ class StateHistory {
return historyIndex[url]; return historyIndex[url];
} }
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? { fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
if(StateApp.instance.privateMode)
return null;
val existing = historyIndex[video.url]; val existing = historyIndex[video.url];
var result: DBHistory.Index? = null; var result: DBHistory.Index? = null;
if(existing != null) { if(existing != null) {
@@ -113,6 +115,19 @@ class StateHistory {
return result; return result;
} }
fun markAsWatched(video: IPlatformVideo) {
try {
val history = getHistoryByVideo(video, true, OffsetDateTime.now());
if (history != null) {
updateHistoryPosition(video, history, true, Math.max(1, video.duration - 1));
}
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to mark as watched", ex);
UIDialogs.toast("Failed to mark as watched\n" + ex.message);
}
}
fun removeHistory(url: String) { fun removeHistory(url: String) {
val hist = getHistoryIndexByUrl(url); val hist = getHistoryIndexByUrl(url);
if(hist != null) if(hist != null)
@@ -93,6 +93,7 @@ class StatePlatform {
private val _channelClientPool = PlatformMultiClientPool("Channels", 15); //Used primarily for subscription/background channel fetches private val _channelClientPool = PlatformMultiClientPool("Channels", 15); //Used primarily for subscription/background channel fetches
private val _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers private val _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
private val _privateClientPool = PlatformMultiClientPool("Private", 2, true); //Used primarily for calls if in incognito mode
private val _icons : HashMap<String, ImageVariable> = HashMap(); private val _icons : HashMap<String, ImageVariable> = HashMap();
@@ -109,13 +110,24 @@ class StatePlatform {
//Batched Requests //Batched Requests
private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope, private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope,
{ url -> { url ->
Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]"); Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]");
_enabledClients.find { it.isContentDetailsUrl(url) }?.let { if(!StateApp.instance.privateMode) {
_mainClientPool.getClientPooled(it).getContentDetails(url) _enabledClients.find { it.isContentDetailsUrl(url) }?.let {
} ?: throw NoPlatformClientException("No client enabled that supports this url ($url)"); _mainClientPool.getClientPooled(it).getContentDetails(url)
}
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
}
else {
Logger.i(TAG, "Fetching details with private client");
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
_privateClientPool.getClientPooled(it).getContentDetails(url)
}
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
}
}, },
{ {
if(!Settings.instance.browsing.videoCache) if(!Settings.instance.browsing.videoCache || StateApp.instance.privateMode)
return@BatchedTaskHandler null; return@BatchedTaskHandler null;
else { else {
val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null; val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null;
@@ -131,7 +143,7 @@ class StatePlatform {
} }
}, },
{ para, result -> { para, result ->
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive)) if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive) || StateApp.instance.privateMode)
return@BatchedTaskHandler return@BatchedTaskHandler
else { else {
Logger.i(TAG, "Caching [${para}]"); Logger.i(TAG, "Caching [${para}]");
@@ -647,6 +659,15 @@ class StatePlatform {
return client.getPlaybackTracker(url); return client.getPlaybackTracker(url);
} }
fun getContentRecommendations(url: String): IPager<IPlatformContent>? {
val baseClient = getContentClientOrNull(url) ?: return null;
if (baseClient !is JSClient) {
return baseClient.getContentRecommendations(url);
}
val client = _mainClientPool.getClientPooled(baseClient);
return client.getContentRecommendations(url);
}
fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) }; fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) };
fun getChannelClient(url : String, exclude: List<String>? = null) : IPlatformClient = getChannelClientOrNull(url, exclude) fun getChannelClient(url : String, exclude: List<String>? = null) : IPlatformClient = getChannelClientOrNull(url, exclude)
?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})"); ?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})");
@@ -862,7 +883,10 @@ class StatePlatform {
if(!client.capabilities.hasGetComments) if(!client.capabilities.hasGetComments)
return EmptyPager(); return EmptyPager();
return client.fromPool(_mainClientPool).getComments(url); if(!StateApp.instance.privateMode)
return client.fromPool(_mainClientPool).getComments(url);
else
return client.fromPool(_privateClientPool).getComments(url);
} }
fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> { fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
Logger.i(TAG, "Platform - getSubComments"); Logger.i(TAG, "Platform - getSubComments");
@@ -873,7 +897,11 @@ class StatePlatform {
fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? { fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? {
Logger.i(TAG, "Platform - getLiveChat"); Logger.i(TAG, "Platform - getLiveChat");
var client = getContentClient(url); var client = getContentClient(url);
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
if(!StateApp.instance.privateMode)
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
else
return client.fromPool(_privateClientPool).getLiveEvents(url);
} }
fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? { fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? {
Logger.i(TAG, "Platform - getLiveChat"); Logger.i(TAG, "Platform - getLiveChat");
@@ -1,7 +1,6 @@
package com.futo.platformplayer.views package com.futo.platformplayer.views
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
@@ -49,6 +48,7 @@ class MonetizationView : LinearLayout {
private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url -> private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url ->
val client = ManagedHttpClient(); val client = ManagedHttpClient();
Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url")
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url") val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
if (!result.isOk) { if (!result.isOk) {
throw Exception("Failed to retrieve store data."); throw Exception("Failed to retrieve store data.");
@@ -15,6 +15,7 @@ import com.futo.platformplayer.R
open class InsertedViewAdapterWithLoader<TViewHolder> : InsertedViewAdapter<TViewHolder> where TViewHolder : ViewHolder { open class InsertedViewAdapterWithLoader<TViewHolder> : InsertedViewAdapter<TViewHolder> where TViewHolder : ViewHolder {
private var _loaderView: ImageView? = null; private var _loaderView: ImageView? = null;
private var _loading = false; private var _loading = false;
val isLoading get() = _loading;
constructor( constructor(
context: Context, context: Context,
@@ -33,6 +33,7 @@ open class PlaylistView : LinearLayout {
protected val _platformIndicator: PlatformIndicator; protected val _platformIndicator: PlatformIndicator;
protected val _textPlaylistName: TextView protected val _textPlaylistName: TextView
protected val _textVideoCount: TextView protected val _textVideoCount: TextView
protected val _textVideoCountLabel: TextView;
protected val _textPlaylistItems: TextView protected val _textPlaylistItems: TextView
protected val _textChannelName: TextView protected val _textChannelName: TextView
protected var _neopassAnimator: ObjectAnimator? = null; protected var _neopassAnimator: ObjectAnimator? = null;
@@ -62,6 +63,7 @@ open class PlaylistView : LinearLayout {
_platformIndicator = findViewById(R.id.thumbnail_platform); _platformIndicator = findViewById(R.id.thumbnail_platform);
_textPlaylistName = findViewById(R.id.text_playlist_name); _textPlaylistName = findViewById(R.id.text_playlist_name);
_textVideoCount = findViewById(R.id.text_video_count); _textVideoCount = findViewById(R.id.text_video_count);
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
_textChannelName = findViewById(R.id.text_channel_name); _textChannelName = findViewById(R.id.text_channel_name);
_textPlaylistItems = findViewById(R.id.text_playlist_items); _textPlaylistItems = findViewById(R.id.text_playlist_items);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel); _imageNeopassChannel = findViewById(R.id.image_neopass_channel);
@@ -137,7 +139,15 @@ open class PlaylistView : LinearLayout {
.crossfade() .crossfade()
.into(_imageThumbnail); .into(_imageThumbnail);
_textVideoCount.text = content.videoCount.toString(); if(content.videoCount >= 0) {
_textVideoCount.text = content.videoCount.toString();
_textVideoCount.visibility = View.VISIBLE;
_textVideoCountLabel.visibility = VISIBLE;
}
else {
_textVideoCount.visibility = View.GONE;
_textVideoCountLabel.visibility = GONE;
}
} }
else { else {
currentPlaylist = null; currentPlaylist = null;
@@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder {
val onRemove = Event1<IPlatformVideo>(); val onRemove = Event1<IPlatformVideo>();
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
constructor(view: View, touchHelper: ItemTouchHelper) : super(view) { constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
_root = view.findViewById(R.id.root); _root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true; _imageThumbnail?.clipToOutline = true;
@@ -59,7 +59,7 @@ class VideoListEditorViewHolder : ViewHolder {
_layoutDownloaded = view.findViewById(R.id.layout_downloaded); _layoutDownloaded = view.findViewById(R.id.layout_downloaded);
_imageDragDrop.setOnTouchListener { _, event -> _imageDragDrop.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) { if (touchHelper != null && event.action == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this); touchHelper.startDrag(this);
} }
false false
@@ -1,12 +1,14 @@
package com.futo.platformplayer.views.adapters.viewholders package com.futo.platformplayer.views.adapters.viewholders
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
@@ -45,10 +47,15 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
override fun bind(value: SelectablePlaylist) { override fun bind(value: SelectablePlaylist) {
_textName.text = value.playlist.name; _textName.text = value.playlist.name;
_textMetadata.text = "${value.playlist.videos.size} " + _view.context.getString(R.string.videos); if(value.playlist.videoCount >= 0) {
_textMetadata.text = "${value.playlist.videoCount} " + _view.context.getString(R.string.videos);
_textMetadata.visibility = View.VISIBLE;
}
else
_textMetadata.visibility = View.GONE;
_checkbox.value = value.selected; _checkbox.value = value.selected;
val thumbnail = value.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(); val thumbnail = value.playlist.thumbnail;
if (thumbnail != null) if (thumbnail != null)
Glide.with(_imageThumbnail) Glide.with(_imageThumbnail)
.load(thumbnail) .load(thumbnail)
@@ -62,6 +69,6 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
} }
class SelectablePlaylist( class SelectablePlaylist(
val playlist: Playlist, val playlist: IPlatformPlaylistDetails,
var selected: Boolean = false var selected: Boolean = false
) { } ) { }
@@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>( class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>(
@@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
return@changeExternalDownloadDirectory; return@changeExternalDownloadDirectory;
} }
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
}; };
} else { } else {
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
} }
} }
} }
@@ -14,6 +14,7 @@ import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
@@ -26,6 +27,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.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.IAudioUrlWidevineSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
@@ -36,17 +38,21 @@ 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.streams.sources.LocalAudioSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
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.streams.sources.VideoUrlSource
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.sources.JSAudioUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.dev.V8RemoteObject
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
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.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.google.gson.Gson
import getHttpDataSourceFactory import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -68,6 +74,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private set; private set;
private var _lastVideoMediaSource: MediaSource? = null; private var _lastVideoMediaSource: MediaSource? = null;
private var _lastGeneratedDash: String? = null;
private var _lastAudioMediaSource: MediaSource? = null; private var _lastAudioMediaSource: MediaSource? = null;
private var _lastSubtitleMediaSource: MediaSource? = null; private var _lastSubtitleMediaSource: MediaSource? = null;
private var _shouldPlaybackRestartOnConnectivity: Boolean = false; private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
@@ -375,6 +382,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private fun swapSourceInternal(videoSource: IVideoSource?) { private fun swapSourceInternal(videoSource: IVideoSource?) {
_lastGeneratedDash = null;
when(videoSource) { when(videoSource) {
is LocalVideoSource -> swapVideoSourceLocal(videoSource); is LocalVideoSource -> swapVideoSourceLocal(videoSource);
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource); is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
@@ -415,7 +423,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if(videoSource.hasItag) { if(videoSource.hasItag) {
//Temporary workaround for Youtube //Temporary workaround for Youtube
try { try {
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource); val results = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
_lastGeneratedDash = results.second;
_lastVideoMediaSource = results.first;
return; return;
} }
//If it fails to create the dash workaround, fallback to standard progressive //If it fails to create the dash workaround, fallback to standard progressive
@@ -635,6 +645,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
when (error.errorCode) { when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> { PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
Logger.w(TAG, null) {
"ERROR BAD HTTP ${cause.responseCode},\n" +
"Video Source: ${lastVideoSource?.toString()}\n" +
"Audio Source: ${lastAudioSource?.toString()}\n" +
"Dash: ${_lastGeneratedDash}"
};
}
onDatasourceError.emit(error); onDatasourceError.emit(error);
} }
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, //PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
@@ -25,6 +25,8 @@ import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.HttpUtil; import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import com.futo.platformplayer.engine.dev.V8RemoteObject;
import com.futo.platformplayer.logging.Logger;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap; import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@@ -45,6 +47,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import kotlinx.serialization.json.Json;
/* /*
* Based on the default ExoPlayer DefaultHttpDataSource * Based on the default ExoPlayer DefaultHttpDataSource
*/ */
@@ -582,6 +586,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
requestHeaders = result.getHeaders(); requestHeaders = result.getHeaders();
} }
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl + "\nHEADERS: [" + V8RemoteObject.Companion.getGsonStandard().toJson(requestHeaders)+ "]", null);
HttpURLConnection connection = openConnection(new URL(requestUrl)); HttpURLConnection connection = openConnection(new URL(requestUrl));
connection.setConnectTimeout(connectTimeoutMillis); connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis); connection.setReadTimeout(readTimeoutMillis);
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#CC111111" />
<corners android:radius="100dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#635DAC"
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
</vector>
+16
View File
@@ -70,6 +70,21 @@
android:visibility="gone" android:visibility="gone"
android:elevation="15dp"> android:elevation="15dp">
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/incognito_button"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_disabled_visible_purple"
android:background="@drawable/background_button_round_black"
android:scaleType="fitCenter"
android:visibility="visible"
android:layout_marginLeft="10dp"
android:layout_marginBottom="10dp"
android:elevation="50dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@id/toast_view" />
<com.futo.platformplayer.views.ToastView <com.futo.platformplayer.views.ToastView
android:id="@+id/toast_view" android:id="@+id/toast_view"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -79,4 +94,5 @@
app:layout_constraintLeft_toLeftOf="@id/fragment_main" app:layout_constraintLeft_toLeftOf="@id/fragment_main"
app:layout_constraintRight_toRightOf="@id/fragment_main" app:layout_constraintRight_toRightOf="@id/fragment_main"
app:layout_constraintBottom_toBottomOf="@id/fragment_main" /> app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
</androidx.constraintlayout.motion.widget.MotionLayout> </androidx.constraintlayout.motion.widget.MotionLayout>
@@ -45,6 +45,7 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="14dp" android:textSize="14dp"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textAlignment="center"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" /> android:layout_marginEnd="30dp" />
@@ -6,6 +6,17 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageButton
android:id="@+id/app_icon"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/foreground" />
<!--
<ImageButton <ImageButton
android:layout_width="35dp" android:layout_width="35dp"
android:layout_height="35dp" android:layout_height="35dp"
@@ -13,13 +24,19 @@
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_construction" /> app:srcCompat="@drawable/ic_construction" />
-->
<!--<ImageButton <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingRight="12dp" android:textSize="22dp"
android:scaleType="fitCenter" android:layout_marginTop="-2dp"
app:srcCompat="@drawable/ic_futo_logo_text" />--> android:fontFamily="@font/inter_light"
android:text="Grayjay"
android:textColor="@color/white"
android:gravity="center_vertical"
android:layout_marginStart="8dp"/>
<!--
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -42,6 +59,8 @@
android:textColor="@color/white" android:textColor="@color/white"
android:layout_marginTop="-8dp"/> android:layout_marginTop="-8dp"/>
</LinearLayout> </LinearLayout>
-->
<Space <Space
android:layout_width="0dp" android:layout_width="0dp"
@@ -15,13 +15,6 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/foreground" /> app:srcCompat="@drawable/foreground" />
<!--<ImageButton
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingRight="12dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_futo_logo_text" />-->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -34,30 +27,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
<!--
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15dp"
android:fontFamily="@font/inter_bold"
android:text="@string/under"
android:textColor="@color/white"
android:layout_marginTop="3dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:fontFamily="@font/inter_bold"
android:text="@string/construction"
android:textColor="@color/white"
android:layout_marginTop="-8dp"/>
</LinearLayout>-->
<Space <Space
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="0dp"
app:layout_scrollFlags="scroll"
app:contentInsetStart="0dp"
app:contentInsetEnd="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="220dp">
<ImageView
android:id="@+id/image_playlist_thumbnail"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/background_thumbnail_live"
android:scaleType="centerCrop" />
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/bottom_gradient"
android:scaleType="fitXY" />
<ImageButton
android:id="@+id/button_share"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/background_button_round"
android:gravity="center"
android:layout_marginStart="5dp"
android:orientation="horizontal"
app:srcCompat="@drawable/ic_share"
app:tint="@color/white"
android:padding="10dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="10dp"
android:scaleType="fitCenter" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="-90dp"
android:layout_marginStart="20dp"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_medium"
android:textColor="@color/white"
android:textSize="18dp"
tools:text="Playlist name"
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
app:layout_constraintBottom_toTopOf="@id/text_metadata"/>
<TextView
android:id="@+id/text_metadata"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_extra_light"
android:textColor="@color/gray_e0"
android:textSize="14dp"
tools:text="3 videos"
android:layout_marginBottom="15dp"
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
app:layout_constraintBottom_toTopOf="@id/container_buttons" />
<LinearLayout
android:id="@+id/container_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="10dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/button_play_all"
android:layout_width="120dp"
android:layout_height="40dp"
android:background="@drawable/background_button_primary_round"
android:gravity="center"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_play_white_nopad"
android:layout_marginEnd="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textColor="@color/white"
android:textSize="16dp"
android:text="@string/play_all" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_shuffle"
android:layout_width="120dp"
android:layout_height="40dp"
android:background="@drawable/background_button_round"
android:gravity="center"
android:layout_marginStart="5dp"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_shuffle"
android:layout_marginEnd="5dp"
app:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textColor="@color/white"
android:textSize="16dp"
android:text="@string/shuffle" />
</LinearLayout>
W
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_playlist"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<FrameLayout
android:id="@+id/layout_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#77000000"
android:visibility="gone">
<ImageView
android:id="@+id/image_loader"
android:layout_width="80dp"
android:layout_height="80dp"
app:srcCompat="@drawable/ic_loader_animated"
android:layout_gravity="center"
android:alpha="0.7"
android:layout_marginTop="80dp"
android:contentDescription="@string/loading" />
</FrameLayout>
</FrameLayout>
@@ -68,6 +68,7 @@
android:textColor="@color/gray_7f"/> android:textColor="@color/gray_7f"/>
<TextView <TextView
android:id="@+id/text_video_count_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="8dp" android:textSize="8dp"
@@ -66,8 +66,8 @@
android:fontFamily="@font/inter_light" android:fontFamily="@font/inter_light"
tools:text="100" tools:text="100"
android:textColor="@color/gray_7f" android:textColor="@color/gray_7f"
app:layout_constraintRight_toLeftOf="@id/text_videos" app:layout_constraintRight_toLeftOf="@id/text_video_count_label"
app:layout_constraintBottom_toBottomOf="@id/text_videos" /> app:layout_constraintBottom_toBottomOf="@id/text_video_count_label" />
<TextView <TextView
android:id="@+id/text_playlist" android:id="@+id/text_playlist"
@@ -80,10 +80,10 @@
android:textColor="@color/gray_e0" android:textColor="@color/gray_e0"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_videos"/> app:layout_constraintBottom_toTopOf="@id/text_video_count_label"/>
<TextView <TextView
android:id="@+id/text_videos" android:id="@+id/text_video_count_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12dp" android:textSize="12dp"
+6
View File
@@ -7,6 +7,7 @@
<string name="add_to">Add to</string> <string name="add_to">Add to</string>
<string name="lorem_ipsum" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string> <string name="lorem_ipsum" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
<string name="add_to_queue">Add to queue</string> <string name="add_to_queue">Add to queue</string>
<string name="add_to_history">Add to history</string>
<string name="general">General</string> <string name="general">General</string>
<string name="channel">Channel</string> <string name="channel">Channel</string>
<string name="home">Home</string> <string name="home">Home</string>
@@ -25,6 +26,7 @@
<string name="sources">Sources</string> <string name="sources">Sources</string>
<string name="buy">Buy</string> <string name="buy">Buy</string>
<string name="faq">FAQ</string> <string name="faq">FAQ</string>
<string name="privacy_mode">Privacy Mode</string>
<string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string> <string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
<string name="defaults">Defaults</string> <string name="defaults">Defaults</string>
<string name="home_screen">Home Screen</string> <string name="home_screen">Home Screen</string>
@@ -66,6 +68,8 @@
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="keep_screen_on">Keep screen on</string> <string name="keep_screen_on">Keep screen on</string>
<string name="keep_screen_on_while_casting">Keep screen on while casting</string> <string name="keep_screen_on_while_casting">Keep screen on while casting</string>
<string name="always_proxy_requests">Always proxy requests</string>
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
<string name="discover">Discover</string> <string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string> <string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string> <string name="these_sources_have_been_disabled">These sources have been disabled</string>
@@ -475,6 +479,8 @@
<string name="removes_all_subscriptions">Removes all subscriptions</string> <string name="removes_all_subscriptions">Removes all subscriptions</string>
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string> <string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
<string name="start_server">Start Server</string> <string name="start_server">Start Server</string>
<string name="test_playback">Test Playback</string>
<string name="test_playback_desc">Keeps playing videos</string>
<string name="subscriptions_cache_5000">Subscriptions Cache 5000</string> <string name="subscriptions_cache_5000">Subscriptions Cache 5000</string>
<string name="history_cache_100">History Cache 100</string> <string name="history_cache_100">History Cache 100</string>
<string name="start_server_on_boot">Start Server on boot</string> <string name="start_server_on_boot">Start Server on boot</string>
+2 -2
View File
@@ -2,12 +2,12 @@
Package http is the main way for a plugin to make web requests, and is likely a package you will always need. Package http is the main way for a plugin to make web requests, and is likely a package you will always need.
It offers several ways to make web requests as well as websocket connections. It offers several ways to make web requests as well as websocket connections.
Before you can use http you need to register it in your plugin config. See [Packages](_blank). Before you can use http you need to register it in your plugin config. See [Packages](/app/src/main/java/com/futo/platformplayer/engine/packages).
## Basic Info ## Basic Info
Underneath the http package by default exist two web clients. An authenticated client and a unauthenticated client. Underneath the http package by default exist two web clients. An authenticated client and a unauthenticated client.
The authenticated client has will apply headers and cookies if the user is logged in with your plugin. The authenticated client has will apply headers and cookies if the user is logged in with your plugin.
See [Plugin Authentication](_blank). See [Plugin Authentication](/docs/Authentication.md).
These two clients are always available even when the user is not logged in, meaning it behaves similar to the unauthenticated client and can safely use it either way. These two clients are always available even when the user is not logged in, meaning it behaves similar to the unauthenticated client and can safely use it either way.
>:warning: **Requests are synchronous** >:warning: **Requests are synchronous**