mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2dce52a5b | |||
| a2c63c59c5 | |||
| 7e54a2ce3d | |||
| 5b7fb2c818 | |||
| da0ac281e2 | |||
| 576b37f64c | |||
| 26c2db5023 | |||
| f344dbf35c | |||
| a04acbd4a5 | |||
| bd48aba8d3 | |||
| 12b73bb248 | |||
| c3ff897ef4 | |||
| 242728fbe7 | |||
| 14df7c8d43 | |||
| 229377bd6e | |||
| d4317ff06f | |||
| c70dbb56c8 | |||
| f9b772b729 | |||
| bbcc424393 | |||
| f433cb1280 | |||
| 9cf81ad20a | |||
| f65e293e45 | |||
| 9a08762e9e | |||
| 66dbd20a90 | |||
| 8254bcc647 | |||
| 51d0f18168 | |||
| 5dcb535c0f | |||
| b7cbeb3837 | |||
| 2067561c09 | |||
| 1ac70dba3f | |||
| f4370c1bfd | |||
| 73321ee362 | |||
| 182c88fc9e | |||
| 9d39d74be5 | |||
| d8d8d6f666 | |||
| df0504cead | |||
| 851b547d64 | |||
| f49ecf1159 | |||
| 081ae1dd88 | |||
| 374d9950be | |||
| 9ffdf39f13 | |||
| 8bb1ff87c0 | |||
| 67e29999ef | |||
| f3f13a71dc | |||
| 5155423a1e | |||
| a7d558e48d | |||
| 7afd75c712 | |||
| 10a661ad4c | |||
| 201fe6f0df | |||
| f76a5b5f01 |
+3
-2
@@ -4,6 +4,7 @@ variables:
|
|||||||
stages:
|
stages:
|
||||||
- buildAndDeployApkUnstable
|
- buildAndDeployApkUnstable
|
||||||
- buildAndDeployApkStable
|
- buildAndDeployApkStable
|
||||||
|
- buildAndDeployPlaystore
|
||||||
|
|
||||||
buildAndDeployApkUnstable:
|
buildAndDeployApkUnstable:
|
||||||
stage: buildAndDeployApkUnstable
|
stage: buildAndDeployApkUnstable
|
||||||
@@ -25,8 +26,8 @@ buildAndDeployApkStable:
|
|||||||
- branches
|
- branches
|
||||||
when: manual
|
when: manual
|
||||||
|
|
||||||
buildAndDeployApkStable:
|
buildAndDeployPlaystore:
|
||||||
stage: buildAndDeployApkStable
|
stage: buildAndDeployPlaystore
|
||||||
script:
|
script:
|
||||||
- sh deploy-playstore.sh
|
- sh deploy-playstore.sh
|
||||||
only:
|
only:
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 29
|
minSdk 28
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|||||||
@@ -127,6 +127,10 @@
|
|||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.CaptchaActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ function pluginUpdateTestPlugin(config) {
|
|||||||
}
|
}
|
||||||
function pluginLoginTestPlugin() {
|
function pluginLoginTestPlugin() {
|
||||||
return syncGET("/plugin/loginTestPlugin", {});
|
return syncGET("/plugin/loginTestPlugin", {});
|
||||||
|
}//captchaLoginTestPlugin
|
||||||
|
function pluginCaptchaTestPlugin(url, html) {
|
||||||
|
return syncPOST("/plugin/captchaTestPlugin?url=" + url, {}, html);
|
||||||
}
|
}
|
||||||
function pluginLogoutTestPlugin() {
|
function pluginLogoutTestPlugin() {
|
||||||
return syncGET("/plugin/logoutTestPlugin", {});
|
return syncGET("/plugin/logoutTestPlugin", {});
|
||||||
|
|||||||
@@ -681,6 +681,9 @@
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
captchaTestPlugin() {
|
||||||
|
captchaLoginTestPlugin();
|
||||||
|
},
|
||||||
logoutTestPlugin() {
|
logoutTestPlugin() {
|
||||||
pluginLogoutTestPlugin();
|
pluginLogoutTestPlugin();
|
||||||
},
|
},
|
||||||
@@ -838,6 +841,12 @@
|
|||||||
this.Testing.lastResultError = "";
|
this.Testing.lastResultError = "";
|
||||||
}
|
}
|
||||||
catch(ex) {
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
console.error("Failed to run test for " + req.title, ex);
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
this.Testing.lastResult = ""
|
this.Testing.lastResult = ""
|
||||||
if(ex.message)
|
if(ex.message)
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ class ScriptException extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class CaptchaRequiredException extends Error {
|
||||||
|
constructor(url, body) {
|
||||||
|
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||||
|
this.plugin_type = "CaptchaRequiredException";
|
||||||
|
this.url = url;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class CriticalException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("CriticalException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
class UnavailableException extends ScriptException {
|
class UnavailableException extends ScriptException {
|
||||||
constructor(msg) {
|
constructor(msg) {
|
||||||
super("UnavailableException", msg);
|
super("UnavailableException", msg);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
|
||||||
|
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
|
||||||
|
|
||||||
|
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
||||||
@@ -4,7 +4,9 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.*
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -43,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Manage Polycentric identity", FieldForm.BUTTON,
|
"Manage Polycentric identity", FieldForm.BUTTON,
|
||||||
"Manage your Polycentric identity", -2
|
"Manage your Polycentric identity", -3
|
||||||
)
|
)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -55,6 +57,19 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(
|
||||||
|
"Open FAQ", FieldForm.BUTTON,
|
||||||
|
"Get answers to common questions", -2
|
||||||
|
)
|
||||||
|
fun openFAQ() {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
|
||||||
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Submit feedback", FieldForm.BUTTON,
|
"Submit feedback", FieldForm.BUTTON,
|
||||||
"Give feedback on the application", -1
|
"Give feedback on the application", -1
|
||||||
@@ -63,7 +78,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
try {
|
try {
|
||||||
val i = Intent(Intent.ACTION_VIEW);
|
val i = Intent(Intent.ACTION_VIEW);
|
||||||
val subject = "Feedback Grayjay";
|
val subject = "Feedback Grayjay";
|
||||||
val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n\n";
|
val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n" +
|
||||||
|
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n";
|
||||||
val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
|
val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
|
||||||
i.data = data;
|
i.data = data;
|
||||||
|
|
||||||
@@ -140,7 +156,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 6)
|
@FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
|
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
|
||||||
@DropdownFieldOptionsId(R.array.background_interval)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||||
|
|
||||||
@@ -156,7 +176,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 7)
|
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var subscriptionConcurrency: Int = 3;
|
var subscriptionConcurrency: Int = 3;
|
||||||
|
|
||||||
@@ -213,7 +233,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||||
|
|
||||||
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "Auto-rotate deadzone in degrees", 5)
|
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
|
||||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||||
var autoRotateDeadZone: Int = 0;
|
var autoRotateDeadZone: Int = 0;
|
||||||
|
|
||||||
@@ -333,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
"Submit logs to help us narrow down issues", 1
|
"Submit logs to help us narrow down issues", 1
|
||||||
)
|
)
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -406,6 +426,33 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FormField("External Storage", FieldForm.GROUP, "", 12)
|
||||||
|
var storage = Storage();
|
||||||
|
@Serializable
|
||||||
|
class Storage {
|
||||||
|
var storage_general: String? = null;
|
||||||
|
var storage_download: String? = null;
|
||||||
|
|
||||||
|
fun getStorageGeneralUri(): Uri? = storage_general?.let { Uri.parse(it) };
|
||||||
|
fun getStorageDownloadUri(): Uri? = storage_download?.let { Uri.parse(it) };
|
||||||
|
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
||||||
|
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
||||||
|
|
||||||
|
@FormField("Change external General directory", FieldForm.BUTTON, "Change the external directory for general files, used for persistent files like auto-backup", 3)
|
||||||
|
fun changeStorageGeneral() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@FormField("Change external Downloads directory", FieldForm.BUTTON, "Change the external storage for download files, used for exported download files", 4)
|
||||||
|
fun changeStorageDownload() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Auto Update", "group", "Configure the auto updater", 12)
|
@FormField("Auto Update", "group", "Configure the auto updater", 12)
|
||||||
var autoUpdate = AutoUpdate();
|
var autoUpdate = AutoUpdate();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -462,7 +509,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
UIDialogs.toast("Retrieving changelog");
|
UIDialogs.toast("Retrieving changelog");
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
||||||
Logger.i(TAG, "Version retrieved $version");
|
Logger.i(TAG, "Version retrieved $version");
|
||||||
@@ -511,7 +558,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1)
|
@FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1)
|
||||||
fun configureAutomaticBackup() {
|
fun configureAutomaticBackup() {
|
||||||
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!);
|
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
||||||
|
SettingsActivity.getActivity()?.reloadSettings();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2)
|
@FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2)
|
||||||
fun restoreAutomaticBackup() {
|
fun restoreAutomaticBackup() {
|
||||||
@@ -542,6 +591,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it, "Licenses cleared, might require app restart");
|
UIDialogs.toast(it, "Licenses cleared, might require app restart");
|
||||||
|
it.reloadSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.futo.platformplayer.casting.StateCasting
|
|||||||
import com.futo.platformplayer.dialogs.*
|
import com.futo.platformplayer.dialogs.*
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -90,11 +92,25 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showAutomaticBackupDialog(context: Context) {
|
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
||||||
val dialog = AutomaticBackupDialog(context);
|
val dialogAction: ()->Unit = {
|
||||||
registerDialogOpened(dialog);
|
val dialog = AutomaticBackupDialog(context);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
registerDialogOpened(dialog);
|
||||||
dialog.show();
|
dialog.setOnDismissListener { registerDialogClosed(dialog); onClosed?.invoke() };
|
||||||
|
dialog.show();
|
||||||
|
};
|
||||||
|
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}), //To nothing
|
||||||
|
UIDialogs.Action("Override", {
|
||||||
|
dialogAction();
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||||
|
UIDialogs.Action("Restore", {
|
||||||
|
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
else {
|
||||||
|
dialogAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) {
|
fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) {
|
||||||
val dialog = AutomaticRestoreDialog(context, scope);
|
val dialog = AutomaticRestoreDialog(context, scope);
|
||||||
@@ -134,10 +150,10 @@ class UIDialogs {
|
|||||||
val buttonView = TextView(context);
|
val buttonView = TextView(context);
|
||||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
||||||
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
||||||
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics);
|
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
||||||
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||||
if(actions.size > 1)
|
if(actions.size > 1)
|
||||||
this.marginEnd = dp28;
|
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
||||||
};
|
};
|
||||||
buttonView.setTextColor(Color.WHITE);
|
buttonView.setTextColor(Color.WHITE);
|
||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
@@ -151,8 +167,9 @@ class UIDialogs {
|
|||||||
ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red))
|
ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red))
|
||||||
else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
||||||
}
|
}
|
||||||
|
val paddingSpecialButtons = if(actions.size > 2) dp14 else dp28;
|
||||||
if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT)
|
if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT)
|
||||||
buttonView.setPadding(dp28, dp10, dp28, dp10);
|
buttonView.setPadding(paddingSpecialButtons, dp10, paddingSpecialButtons, dp10);
|
||||||
else
|
else
|
||||||
buttonView.setPadding(dp10, dp10, dp10, dp10);
|
buttonView.setPadding(dp10, dp10, dp10, dp10);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.ContentResolver
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
|
||||||
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.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
|
||||||
@@ -18,6 +17,7 @@ import com.futo.platformplayer.helpers.VideoHelper
|
|||||||
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.*
|
import com.futo.platformplayer.states.*
|
||||||
|
import com.futo.platformplayer.views.Loader
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
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
|
||||||
@@ -29,7 +29,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -45,7 +45,7 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDownloadVideoOverlay(contentResolver: ContentResolver, video: IPlatformVideoDetails, container: ViewGroup): SlideUpMenuOverlay? {
|
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
@@ -68,6 +68,12 @@ class UISlideOverlays {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!VideoHelper.isDownloadable(video)) {
|
||||||
|
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
||||||
|
UIDialogs.toast( "No downloadable sources (yet)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
||||||
selectedVideo = null;
|
selectedVideo = null;
|
||||||
@@ -76,7 +82,7 @@ class UISlideOverlays {
|
|||||||
menu?.setOk("Download");
|
menu?.setOk("Download");
|
||||||
}, false)) +
|
}, false)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it is IVideoUrlSource }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
selectedVideo = it as IVideoUrlSource;
|
selectedVideo = it as IVideoUrlSource;
|
||||||
@@ -88,14 +94,14 @@ class UISlideOverlays {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it is IVideoUrlSource }.asIterable(),
|
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
||||||
|
|
||||||
|
|
||||||
audioSources?.let { audioSources ->
|
audioSources?.let { audioSources ->
|
||||||
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
||||||
.filter { it is IAudioUrlSource }
|
.filter { VideoHelper.isDownloadable(it) }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||||
selectedAudio = it as IAudioUrlSource;
|
selectedAudio = it as IAudioUrlSource;
|
||||||
@@ -111,24 +117,27 @@ class UISlideOverlays {
|
|||||||
menu?.selectOption(asources, preferredAudioSource);
|
menu?.selectOption(asources, preferredAudioSource);
|
||||||
|
|
||||||
|
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
//ContentResolver is required for subtitles..
|
||||||
.map {
|
if(contentResolver != null) {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
||||||
if (selectedSubtitle == it) {
|
.map {
|
||||||
selectedSubtitle = null;
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
menu?.selectOption(subtitleSources, null);
|
if (selectedSubtitle == it) {
|
||||||
} else {
|
selectedSubtitle = null;
|
||||||
selectedSubtitle = it;
|
menu?.selectOption(subtitleSources, null);
|
||||||
menu?.selectOption(subtitleSources, it);
|
} else {
|
||||||
}
|
selectedSubtitle = it;
|
||||||
}, false);
|
menu?.selectOption(subtitleSources, it);
|
||||||
}));
|
}
|
||||||
|
}, false);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items);
|
menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items);
|
||||||
|
|
||||||
@@ -153,29 +162,12 @@ class UISlideOverlays {
|
|||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
||||||
if (subtitleUri != null) {
|
//TODO: Remove uri dependency, should be able to work with raw aswell?
|
||||||
var subtitles: String? = null;
|
if (subtitleUri != null && contentResolver != null) {
|
||||||
if ("file" == subtitleUri.scheme) {
|
val subtitlesRaw = StateDownloads.instance.downloadSubtitles(subtitleToDownload, contentResolver);
|
||||||
val inputStream = contentResolver.openInputStream(subtitleUri);
|
|
||||||
inputStream?.use { stream ->
|
|
||||||
val reader = stream.bufferedReader();
|
|
||||||
subtitles = reader.use { it.readText() };
|
|
||||||
}
|
|
||||||
} else if ("http" == subtitleUri.scheme || "https" == subtitleUri.scheme) {
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val subtitleResponse = client.get(subtitleUri.toString());
|
|
||||||
if (!subtitleResponse.isOk) {
|
|
||||||
throw Exception("Cannot fetch subtitles from source '${subtitleUri}': ${subtitleResponse.code}");
|
|
||||||
}
|
|
||||||
|
|
||||||
subtitles = subtitleResponse.body?.toString()
|
|
||||||
?: throw Exception("Subtitles are invalid '${subtitleUri}': ${subtitleResponse.code}");
|
|
||||||
} else {
|
|
||||||
throw Exception("Unsuported scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
StateDownloads.instance.download(video, selectedVideo, selectedAudio, if (subtitles != null) SubtitleRawSource(subtitleToDownload.name, subtitleToDownload.format, subtitles!!) else null);
|
StateDownloads.instance.download(video, selectedVideo, selectedAudio, subtitlesRaw);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -191,10 +183,41 @@ class UISlideOverlays {
|
|||||||
};
|
};
|
||||||
return menu.apply { show() };
|
return menu.apply { show() };
|
||||||
}
|
}
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup) {
|
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
val handleUnknownDownload: ()->Unit = {
|
||||||
StateDownloads.instance.download(video, px, bitrate)
|
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
||||||
|
StateDownloads.instance.download(video, px, bitrate)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
if(!useDetails)
|
||||||
|
handleUnknownDownload();
|
||||||
|
else {
|
||||||
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
|
||||||
|
if(scope != null) {
|
||||||
|
val loader = showLoaderOverlay("Fetching video details", container);
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
||||||
|
if(videoDetails !is IPlatformVideoDetails)
|
||||||
|
throw IllegalStateException("Not a video details");
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(showDownloadVideoOverlay(videoDetails, container, StateApp.instance.contextOrNull?.contentResolver) == null)
|
||||||
|
loader.hide(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast("Failed to fetch details for download");
|
||||||
|
handleUnknownDownload();
|
||||||
|
loader.hide(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else handleUnknownDownload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
||||||
@@ -269,6 +292,18 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showLoaderOverlay(text: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
|
val dp70 = 70.dp(container.context.resources);
|
||||||
|
val dp15 = 15.dp(container.context.resources);
|
||||||
|
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
||||||
|
Loader(container.context, true, dp70).apply {
|
||||||
|
this.setPadding(0, dp15, 0, dp15);
|
||||||
|
}
|
||||||
|
), true);
|
||||||
|
overlay.show();
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
@@ -291,7 +326,7 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
||||||
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
||||||
{ showDownloadVideoOverlay(video, container); }, false)
|
{ showDownloadVideoOverlay(video, container, true); }, false)
|
||||||
))
|
))
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Add To", "addto",
|
SlideUpMenuGroup(container.context, "Add To", "addto",
|
||||||
@@ -344,7 +379,7 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} videos", "watch later",
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} videos", "watch later",
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
||||||
{ showDownloadVideoOverlay(video, container); }, false))
|
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.icu.util.Output
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
@@ -15,6 +16,7 @@ import android.view.WindowInsetsController
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||||
@@ -56,7 +58,7 @@ fun findNonRuntimeException(ex: Throwable?): Throwable? {
|
|||||||
|
|
||||||
fun warnIfMainThread(context: String) {
|
fun warnIfMainThread(context: String) {
|
||||||
if(BuildConfig.DEBUG && Looper.myLooper() == Looper.getMainLooper())
|
if(BuildConfig.DEBUG && Looper.myLooper() == Looper.getMainLooper())
|
||||||
Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace);
|
Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace.joinToString { it.toString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureNotMainThread() {
|
fun ensureNotMainThread() {
|
||||||
@@ -75,6 +77,16 @@ fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPool
|
|||||||
|
|
||||||
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
||||||
|
|
||||||
|
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
||||||
|
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
||||||
|
fun DocumentFile.copyTo(context: Context, file: DocumentFile) = this.getInputStream(context).use { input ->
|
||||||
|
file.getOutputStream(context)?.use { output -> input?.copyTo(output) }
|
||||||
|
};
|
||||||
|
fun DocumentFile.readBytes(context: Context) = this.getInputStream(context).use { input -> input?.readBytes() };
|
||||||
|
fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.contentResolver.openOutputStream(this.uri)?.use {
|
||||||
|
it.write(byteArray);
|
||||||
|
it.flush();
|
||||||
|
};
|
||||||
|
|
||||||
fun loadBitmap(url: String): Bitmap {
|
fun loadBitmap(url: String): Bitmap {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
onBackPressed();
|
finish();
|
||||||
};
|
};
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
onBackPressed();
|
finish();
|
||||||
}
|
}
|
||||||
_buttonInstall.setOnClickListener {
|
_buttonInstall.setOnClickListener {
|
||||||
_config?.let {
|
_config?.let {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
@@ -14,6 +17,31 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
|
|
||||||
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
val content = it.contents
|
||||||
|
if (content == null) {
|
||||||
|
UIDialogs.toast(this, "Failed to scan QR code")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = if (content.startsWith("https://")) {
|
||||||
|
content
|
||||||
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
|
content.substring("grayjay://plugin/".length)
|
||||||
|
} else {
|
||||||
|
UIDialogs.toast(this, "Not a plugin URL")
|
||||||
|
return@let;
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url);
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
@@ -37,8 +65,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
integrator.setBarcodeImageEnabled(true)
|
integrator.setBarcodeImageEnabled(true)
|
||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
integrator.initiateScan()
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, "Not implemented yet..");
|
UIDialogs.toast(this, "Not implemented yet..");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginCaptchaConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.others.CaptchaWebViewClient
|
||||||
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class CaptchaActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _webView: WebView;
|
||||||
|
private lateinit var _buttonClose: Button;
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_captcha);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener { finish(); };
|
||||||
|
|
||||||
|
_webView = findViewById(R.id.web_view);
|
||||||
|
_webView.settings.javaScriptEnabled = true;
|
||||||
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
|
|
||||||
|
|
||||||
|
val config = if(intent.hasExtra("plugin"))
|
||||||
|
Json.decodeFromString<SourcePluginConfig>(intent.getStringExtra("plugin")!!);
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val captchaConfig = if(config != null)
|
||||||
|
config.captcha ?: throw IllegalStateException("Plugin has no captcha support");
|
||||||
|
else if(intent.hasExtra("captcha"))
|
||||||
|
Json.decodeFromString<SourcePluginCaptchaConfig>(intent.getStringExtra("captcha")!!);
|
||||||
|
else throw IllegalStateException("No valid configuration?");
|
||||||
|
//TODO: Backwards compat removal?
|
||||||
|
|
||||||
|
val extraUrl = if (intent.hasExtra("url"))
|
||||||
|
intent.getStringExtra("url");
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val extraBody = if (intent.hasExtra("body"))
|
||||||
|
intent.getStringExtra("body");
|
||||||
|
else null;
|
||||||
|
|
||||||
|
_webView.settings.userAgentString = captchaConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
|
||||||
|
_webView.settings.useWideViewPort = true;
|
||||||
|
_webView.settings.loadWithOverviewMode = true;
|
||||||
|
|
||||||
|
val webViewClient = if(config != null) CaptchaWebViewClient(config) else CaptchaWebViewClient(captchaConfig);
|
||||||
|
webViewClient.onCaptchaFinished.subscribe { captcha ->
|
||||||
|
_callback?.let {
|
||||||
|
_callback = null;
|
||||||
|
it.invoke(captcha);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
};
|
||||||
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
_webView.webViewClient = webViewClient;
|
||||||
|
|
||||||
|
if(captchaConfig.captchaUrl != null)
|
||||||
|
_webView.loadUrl(captchaConfig.captchaUrl);
|
||||||
|
else if(extraUrl != null && extraBody != null)
|
||||||
|
_webView.loadDataWithBaseURL(extraUrl, extraBody, "text/html", "utf-8", null);
|
||||||
|
else if(extraUrl != null)
|
||||||
|
_webView.loadUrl(extraUrl);
|
||||||
|
else throw IllegalStateException("No valid captcha info provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
_webView.loadUrl("about:blank");
|
||||||
|
}
|
||||||
|
_callback?.let {
|
||||||
|
_callback = null;
|
||||||
|
it.invoke(null);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CaptchaActivity";
|
||||||
|
private var _callback: ((SourceCaptchaData?) -> Unit)? = null;
|
||||||
|
|
||||||
|
private fun getCaptchaIntent(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null): Intent {
|
||||||
|
val intent = Intent(context, CaptchaActivity::class.java);
|
||||||
|
if(url != null)
|
||||||
|
intent.putExtra("url", url);
|
||||||
|
if(body != null)
|
||||||
|
intent.putExtra("body", body);
|
||||||
|
intent.putExtra("plugin", Json.encodeToString(config));
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showCaptcha(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null, callback: ((SourceCaptchaData?) -> Unit)? = null) {
|
||||||
|
_callback = callback;
|
||||||
|
context.startActivity(getCaptchaIntent(context, config, url, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -40,7 +41,8 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
||||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
|
||||||
|
|
||||||
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n\n" +
|
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
|
||||||
|
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
|
||||||
Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack");
|
Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack");
|
||||||
try {
|
try {
|
||||||
val file = File(filesDir, "log.txt");
|
val file = File(filesDir, "log.txt");
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.futo.platformplayer.activities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -68,9 +70,15 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO: Required for some...TBD what to do with it. Clear on finish?
|
|
||||||
_webView.settings.domStorageEnabled = true;
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
_webView.webChromeClient = object: WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
Logger.w(TAG, "Login Console: " + consoleMessage?.message());
|
||||||
|
return super.onConsoleMessage(consoleMessage);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
_webView.webViewClient = webViewClient;
|
_webView.webViewClient = webViewClient;
|
||||||
_webView.loadUrl(authConfig.loginUrl);
|
_webView.loadUrl(authConfig.loginUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.i(TAG, "onResume")
|
Logger.v(TAG, "onResume")
|
||||||
|
|
||||||
val curOrientation = _orientationManager.orientation;
|
val curOrientation = _orientationManager.orientation;
|
||||||
|
|
||||||
@@ -408,13 +408,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
val videoToOpen = StateSaved.instance.videoToOpen;
|
val videoToOpen = StateSaved.instance.videoToOpen;
|
||||||
|
|
||||||
if (_wasStopped) {
|
if (_wasStopped) {
|
||||||
Logger.i(TAG, "_wasStopped is true");
|
|
||||||
Logger.i(TAG, "set _wasStopped = false");
|
|
||||||
_wasStopped = false;
|
_wasStopped = false;
|
||||||
|
|
||||||
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
|
||||||
|
|
||||||
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
|
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
@@ -427,13 +424,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.i(TAG, "onPause")
|
Logger.v(TAG, "onPause")
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
Logger.i(TAG, "_wasStopped = true");
|
Logger.v(TAG, "_wasStopped = true");
|
||||||
_wasStopped = true;
|
_wasStopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,6 +607,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
val name = when(type) {
|
val name = when(type) {
|
||||||
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
||||||
else -> type
|
else -> type
|
||||||
@@ -722,22 +720,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
_fragVideoDetail.onOrientationChanged(OrientationManager.Orientation.PORTRAIT);
|
_fragVideoDetail.onOrientationChanged(OrientationManager.Orientation.PORTRAIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "onRestart5");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
|
||||||
|
|
||||||
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
||||||
Logger.i(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
||||||
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
||||||
Logger.i(TAG, "onPictureInPictureModeChanged Ready");
|
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Logger.i(TAG, "onDestroy")
|
Logger.v(TAG, "onDestroy")
|
||||||
|
|
||||||
_orientationManager.disable();
|
_orientationManager.disable();
|
||||||
|
|
||||||
@@ -899,7 +895,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
private var requestCode: Int? = -1;
|
private var requestCode: Int? = -1;
|
||||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
|
|||||||
+25
-16
@@ -5,6 +5,7 @@ import android.os.Bundle
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -14,6 +15,7 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
|
|||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.*
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -27,6 +29,16 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportProfile: LinearLayout;
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _editProfile: EditText;
|
||||||
|
|
||||||
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
if (it.contents != null) {
|
||||||
|
val scannedUrl = it.contents
|
||||||
|
import(scannedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_import_profile);
|
setContentView(R.layout.activity_polycentric_import_profile);
|
||||||
@@ -45,10 +57,15 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this);
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE);
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt("Scan a QR code");
|
integrator.setPrompt("Scan a QR code")
|
||||||
integrator.initiateScan();
|
integrator.setOrientationLocked(true);
|
||||||
|
integrator.setCameraId(0)
|
||||||
|
integrator.setBeepEnabled(false)
|
||||||
|
integrator.setBarcodeImageEnabled(true)
|
||||||
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
@@ -66,18 +83,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
|
||||||
if (result != null) {
|
|
||||||
if (result.contents != null) {
|
|
||||||
val scannedUrl = result.contents;
|
|
||||||
import(scannedUrl);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, "Not a valid URL");
|
UIDialogs.toast(this, "Not a valid URL");
|
||||||
@@ -126,4 +131,8 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity";
|
private const val TAG = "PolycentricImportProfileActivity";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class QRCaptureActivity: CaptureActivity() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,10 @@ 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
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.views.Loader
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
@@ -18,6 +21,7 @@ import com.google.android.material.button.MaterialButton
|
|||||||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
private lateinit var _loader: Loader;
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
private lateinit var _devSets: LinearLayout;
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
private lateinit var _buttonDev: MaterialButton;
|
||||||
@@ -33,9 +37,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
|
_loader = findViewById(R.id.loader);
|
||||||
|
|
||||||
_form.fromObject(Settings.instance);
|
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { field, value ->
|
||||||
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
};
|
};
|
||||||
@@ -47,18 +52,28 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
startActivity(Intent(this, DeveloperActivity::class.java));
|
startActivity(Intent(this, DeveloperActivity::class.java));
|
||||||
}
|
}
|
||||||
|
|
||||||
var devCounter = 0;
|
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
|
||||||
devCounter++;
|
|
||||||
if(devCounter > 5) {
|
|
||||||
devCounter = 0;
|
|
||||||
SettingsDev.instance.developerMode = true;
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
updateDevMode();
|
|
||||||
UIDialogs.toast(this, "You are now in developer mode");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_lastActivity = this;
|
_lastActivity = this;
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadSettings() {
|
||||||
|
_loader.start();
|
||||||
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
|
_loader.stop();
|
||||||
|
|
||||||
|
var devCounter = 0;
|
||||||
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
|
devCounter++;
|
||||||
|
if(devCounter > 5) {
|
||||||
|
devCounter = 0;
|
||||||
|
SettingsDev.instance.developerMode = true;
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
updateDevMode();
|
||||||
|
UIDialogs.toast(this, "You are now in developer mode");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -28,7 +29,11 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
client = builder.build();
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
|
val request = beforeRequest(chain.request());
|
||||||
|
val response = afterRequest(chain.proceed(request));
|
||||||
|
return@addNetworkInterceptor response;
|
||||||
|
}.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun clone(): ManagedHttpClient {
|
open fun clone(): ManagedHttpClient {
|
||||||
@@ -116,7 +121,7 @@ open class ManagedHttpClient {
|
|||||||
fun execute(request : Request) : Response {
|
fun execute(request : Request) : Response {
|
||||||
ensureNotMainThread();
|
ensureNotMainThread();
|
||||||
|
|
||||||
beforeRequest(request);
|
//beforeRequest(request);
|
||||||
|
|
||||||
Logger.v(TAG, "HTTP Request [${request.method}] ${request.url} - [${if(request.body != null) request.body.size else 0}]");
|
Logger.v(TAG, "HTTP Request [${request.method}] ${request.url} - [${if(request.body != null) request.body.size else 0}]");
|
||||||
|
|
||||||
@@ -156,23 +161,16 @@ open class ManagedHttpClient {
|
|||||||
if(true)
|
if(true)
|
||||||
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
|
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
|
||||||
|
|
||||||
afterRequest(request, resp);
|
//afterRequest(request, resp);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Set Listeners
|
//Set Listeners
|
||||||
fun setOnBeforeRequest(listener : (Request)->Unit) {
|
open fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
this.onBeforeRequest = listener;
|
return request;
|
||||||
}
|
}
|
||||||
fun setOnAfterRequest(listener : (Request, Response)->Unit) {
|
open fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||||
this.onAfterRequest = listener;
|
return resp;
|
||||||
}
|
|
||||||
|
|
||||||
open fun beforeRequest(request: Request) {
|
|
||||||
onBeforeRequest?.invoke(request);
|
|
||||||
}
|
|
||||||
open fun afterRequest(request: Request, resp: Response) {
|
|
||||||
onAfterRequest?.invoke(request, resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
|
||||||
Logger.i(TAG, "Started ${port}. \n" + getAddresses().map { it.hostAddress }.joinToString("\n"));
|
Logger.i(TAG, "Started HTTP Server ${port}. \n" + getAddresses().map { it.hostAddress }.joinToString("\n"));
|
||||||
}
|
}
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ class LiveChatManager {
|
|||||||
if(_pager is JSLiveEventPager)
|
if(_pager is JSLiveEventPager)
|
||||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
|
|
||||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
if(newEvents.size > 0)
|
||||||
|
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||||
|
else
|
||||||
|
Logger.v(TAG, "No new Live Events");
|
||||||
|
|
||||||
_scope.launch(Dispatchers.Main) {
|
_scope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
class PlatformClientPool {
|
class PlatformClientPool {
|
||||||
private val _parent: JSClient;
|
private val _parent: JSClient;
|
||||||
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?;
|
||||||
|
|
||||||
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) {
|
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||||
|
_poolName = name;
|
||||||
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");
|
||||||
@@ -47,8 +50,13 @@ class PlatformClientPool {
|
|||||||
_poolCounter++;
|
_poolCounter++;
|
||||||
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 (${_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();
|
||||||
|
|
||||||
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
};
|
||||||
|
|
||||||
reserved?.initialize();
|
reserved?.initialize();
|
||||||
_pool[reserved!!] = _poolCounter;
|
_pool[reserved!!] = _poolCounter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.futo.platformplayer.api.media
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
class PlatformMultiClientPool {
|
class PlatformMultiClientPool {
|
||||||
|
private val _name: String;
|
||||||
private val _maxCap: Int;
|
private val _maxCap: Int;
|
||||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||||
|
|
||||||
private var _isFake = false;
|
private var _isFake = false;
|
||||||
|
|
||||||
constructor(maxCap: Int = -1) {
|
constructor(name: String, maxCap: Int = -1) {
|
||||||
|
_name = name;
|
||||||
_maxCap = if(maxCap > 0)
|
_maxCap = if(maxCap > 0)
|
||||||
maxCap
|
maxCap
|
||||||
else 99;
|
else 99;
|
||||||
@@ -17,7 +19,7 @@ class PlatformMultiClientPool {
|
|||||||
return parentClient;
|
return parentClient;
|
||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||||
this.onDead.subscribe { client, pool ->
|
this.onDead.subscribe { client, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_VIDEOS = "VIDEOS";
|
const val TYPE_VIDEOS = "VIDEOS";
|
||||||
const val TYPE_STREAMS = "STREAMS";
|
const val TYPE_STREAMS = "STREAMS";
|
||||||
const val TYPE_LIVE = "LIVE";
|
const val TYPE_LIVE = "LIVE";
|
||||||
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|||||||
+4
@@ -39,4 +39,8 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MAX_COMMENT_SIZE = 2000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -4,7 +4,7 @@ import com.futo.platformplayer.api.media.PlatformID
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
|
class PlatformContentPlaceholder(pluginId: String, exception: Throwable? = null): IPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.PLACEHOLDER;
|
override val contentType: ContentType = ContentType.PLACEHOLDER;
|
||||||
override val id: PlatformID = PlatformID("", null, pluginId);
|
override val id: PlatformID = PlatformID("", null, pluginId);
|
||||||
override val name: String = "";
|
override val name: String = "";
|
||||||
@@ -12,4 +12,5 @@ class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
|
|||||||
override val shareUrl: String = "";
|
override val shareUrl: String = "";
|
||||||
override val datetime: OffsetDateTime? = null;
|
override val datetime: OffsetDateTime? = null;
|
||||||
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
|
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
|
||||||
|
val error: Throwable? = exception
|
||||||
}
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
|||||||
override val contentProvider: String?,
|
override val contentProvider: String?,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformNestedContent, SerializedPlatformContent {
|
) : IPlatformNestedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
|
||||||
|
|
||||||
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
||||||
override val contentSupported: Boolean get() = contentPlugin != null;
|
override val contentSupported: Boolean get() = contentPlugin != null;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DevJSClient : JSClient {
|
class DevJSClient : JSClient {
|
||||||
@@ -15,29 +16,44 @@ class DevJSClient : JSClient {
|
|||||||
|
|
||||||
private val _devScript: String;
|
private val _devScript: String;
|
||||||
private var _auth: SourceAuth? = null;
|
private var _auth: SourceAuth? = null;
|
||||||
|
private var _captcha: SourceCaptchaData? = null;
|
||||||
|
|
||||||
val devID: String;
|
val devID: String;
|
||||||
|
|
||||||
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), listOf("DEV")), null, script) {
|
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
|
onCaptchaException.subscribe { client, captcha ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, captcha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
|
//TODO: Misisng auth/captcha pass on purpose?
|
||||||
|
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
|
onCaptchaException.subscribe { client, captcha ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, captcha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCaptcha(captcha: SourceCaptchaData? = null) {
|
||||||
|
_captcha = captcha;
|
||||||
|
}
|
||||||
fun setAuth(auth: SourceAuth? = null) {
|
fun setAuth(auth: SourceAuth? = null) {
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
}
|
}
|
||||||
fun recreate(context: Context): DevJSClient {
|
fun recreate(context: Context): DevJSClient {
|
||||||
return DevJSClient(context, config, _devScript, _auth, devID);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(): JSClient {
|
override fun getCopy(): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, _auth, saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueNull
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
@@ -23,9 +24,14 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.*
|
import com.futo.platformplayer.api.media.platforms.js.models.*
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -59,6 +65,7 @@ open class JSClient : IPlatformClient {
|
|||||||
private var _enabled: Boolean = false;
|
private var _enabled: Boolean = false;
|
||||||
|
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
private val _injectedSaveState: String?;
|
private val _injectedSaveState: String?;
|
||||||
|
|
||||||
@@ -85,6 +92,7 @@ open class JSClient : IPlatformClient {
|
|||||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||||
|
|
||||||
val onDisabled = Event1<JSClient>();
|
val onDisabled = Event1<JSClient>();
|
||||||
|
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
||||||
|
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -93,10 +101,11 @@ open class JSClient : IPlatformClient {
|
|||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this);
|
_client = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth);
|
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
@@ -108,6 +117,11 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available");
|
throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available");
|
||||||
|
|
||||||
|
_plugin.onScriptException.subscribe {
|
||||||
|
if(it is ScriptCaptchaRequiredException)
|
||||||
|
onCaptchaException.emit(this, it);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -116,15 +130,21 @@ open class JSClient : IPlatformClient {
|
|||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this);
|
_client = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth);
|
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
_plugin.withScript(script);
|
_plugin.withScript(script);
|
||||||
_script = script;
|
_script = script;
|
||||||
|
|
||||||
|
_plugin.onScriptException.subscribe {
|
||||||
|
if(it is ScriptCaptchaRequiredException)
|
||||||
|
onCaptchaException.emit(this, it);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
@@ -413,8 +433,11 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSCommentPager(config, plugin,
|
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||||
plugin.executeTyped("source.getComments(${Json.encodeToString(url)})"));
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
|
return@isBusyWith EmptyPager<IPlatformComment>();
|
||||||
|
}
|
||||||
|
return@isBusyWith JSCommentPager(config, plugin, pager);
|
||||||
}
|
}
|
||||||
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
||||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||||
@@ -561,11 +584,13 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||||
|
if(ex is PluginEngineException)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
|
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
|
||||||
"Plugin ${config.name} encountered an error in [${method}]",
|
"Plugin ${config.name} encountered an error in [${method}]",
|
||||||
"${ex.message}\nPlease contact the plugin developer",
|
"${ex.message}\nPlease contact the plugin developer",
|
||||||
AnnouncementType.RECURRING,
|
AnnouncementType.SESSION_RECURRING,
|
||||||
OffsetDateTime.now());
|
OffsetDateTime.now());
|
||||||
}
|
}
|
||||||
catch(_: Throwable) {}
|
catch(_: Throwable) {}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toEncrypted(): String{
|
||||||
|
return EncryptionProvider.instance.encrypt(serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serialize(): String {
|
||||||
|
return Json.encodeToString(SerializedCaptchaData(cookieMap, headers));
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "SourceAuth";
|
||||||
|
|
||||||
|
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||||
|
if(encrypted == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
||||||
|
try {
|
||||||
|
return deserialize(decrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(str: String): SourceCaptchaData {
|
||||||
|
val data = Json.decodeFromString<SerializedCaptchaData>(str);
|
||||||
|
return SourceCaptchaData(data.cookieMap, data.headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SerializedCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>?,
|
||||||
|
val headers: Map<String, Map<String, String>> = mapOf())
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SourcePluginCaptchaConfig(
|
||||||
|
val captchaUrl: String? = null,
|
||||||
|
val completionUrl: String? = null,
|
||||||
|
val cookiesToFind: List<String>? = null,
|
||||||
|
val userAgent: String? = null,
|
||||||
|
val cookiesExclOthers: Boolean = true
|
||||||
|
)
|
||||||
@@ -35,11 +35,13 @@ class SourcePluginConfig(
|
|||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
|
var captcha: SourcePluginCaptchaConfig? = null,
|
||||||
val authentication: SourcePluginAuthConfig? = null,
|
val authentication: SourcePluginAuthConfig? = null,
|
||||||
var sourceUrl: String? = null,
|
var sourceUrl: String? = null,
|
||||||
val constants: HashMap<String, String> = hashMapOf(),
|
val constants: HashMap<String, String> = hashMapOf(),
|
||||||
|
|
||||||
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
||||||
|
var subscriptionRateLimit: Int? = null,
|
||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf()
|
var supportedClaimTypes: List<Int> = listOf()
|
||||||
|
|||||||
+16
-3
@@ -13,22 +13,28 @@ class SourcePluginDescriptor {
|
|||||||
|
|
||||||
var appSettings: AppPluginSettings = AppPluginSettings();
|
var appSettings: AppPluginSettings = AppPluginSettings();
|
||||||
|
|
||||||
var authEncrypted: String?
|
var authEncrypted: String? = null
|
||||||
|
private set;
|
||||||
|
var captchaEncrypted: String? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
val flags: List<String>;
|
val flags: List<String>;
|
||||||
|
|
||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val onAuthChanged = Event0();
|
val onAuthChanged = Event0();
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
val onCaptchaChanged = Event0();
|
||||||
|
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = listOf();
|
this.flags = listOf();
|
||||||
}
|
}
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, flags: List<String>) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +47,13 @@ class SourcePluginDescriptor {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCaptcha(captcha: SourceCaptchaData?) {
|
||||||
|
captchaEncrypted = captcha?.toEncrypted();
|
||||||
|
onCaptchaChanged.emit();
|
||||||
|
}
|
||||||
|
fun getCaptchaData(): SourceCaptchaData? {
|
||||||
|
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
fun updateAuth(str: SourceAuth?) {
|
fun updateAuth(str: SourceAuth?) {
|
||||||
authEncrypted = str?.toEncrypted();
|
authEncrypted = str?.toEncrypted();
|
||||||
|
|||||||
+69
-45
@@ -5,90 +5,108 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
var doUpdateCookies: Boolean = true;
|
var doUpdateCookies: Boolean = true;
|
||||||
var doApplyCookies: Boolean = true;
|
var doApplyCookies: Boolean = true;
|
||||||
var doAllowNewCookies: Boolean = true;
|
var doAllowNewCookies: Boolean = true;
|
||||||
val isLoggedIn: Boolean get() = _auth != null;
|
val isLoggedIn: Boolean get() = _auth != null;
|
||||||
|
|
||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>?;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
|
||||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null) : super() {
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null) : super() {
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
|
|
||||||
|
_currentCookieMap = hashMapOf();
|
||||||
if(!auth?.cookieMap.isNullOrEmpty()) {
|
if(!auth?.cookieMap.isNullOrEmpty()) {
|
||||||
_currentCookieMap = hashMapOf();
|
|
||||||
for(domainCookies in auth!!.cookieMap!!)
|
for(domainCookies in auth!!.cookieMap!!)
|
||||||
_currentCookieMap!!.put(domainCookies.key, HashMap(domainCookies.value));
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
}
|
}
|
||||||
else _currentCookieMap = null;
|
if(!captcha?.cookieMap.isNullOrEmpty()) {
|
||||||
|
for(domainCookies in captcha!!.cookieMap!!) {
|
||||||
|
if(_currentCookieMap.containsKey(domainCookies.key))
|
||||||
|
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
|
||||||
|
else
|
||||||
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clone(): ManagedHttpClient {
|
override fun clone(): ManagedHttpClient {
|
||||||
val newClient = JSHttpClient(_jsClient, _auth);
|
val newClient = JSHttpClient(_jsClient, _auth);
|
||||||
newClient._currentCookieMap = if(_currentCookieMap != null)
|
newClient._currentCookieMap = if(_currentCookieMap != null)
|
||||||
HashMap(_currentCookieMap!!.toList().associate { Pair(it.first, HashMap(it.second)) })
|
HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
||||||
else
|
else
|
||||||
null;
|
hashMapOf();
|
||||||
return newClient;
|
return newClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeRequest(request: Request) {
|
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
|
val domain = request.url.host.lowercase();
|
||||||
val auth = _auth;
|
val auth = _auth;
|
||||||
if (auth != null) {
|
|
||||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
|
||||||
|
|
||||||
|
val newBuilder = if(auth != null || doApplyCookies)
|
||||||
|
request.newBuilder();
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
if (auth != null) {
|
||||||
//TODO: Possibly add doApplyHeaders
|
//TODO: Possibly add doApplyHeaders
|
||||||
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
||||||
request.headers[header.key] = header.value;
|
newBuilder?.header(header.key, header.value);
|
||||||
|
}
|
||||||
|
|
||||||
if(doApplyCookies) {
|
if(doApplyCookies) {
|
||||||
if (!_currentCookieMap.isNullOrEmpty()) {
|
if (!_currentCookieMap.isNullOrEmpty()) {
|
||||||
val cookiesToApply = hashMapOf<String, String>();
|
val cookiesToApply = hashMapOf<String, String>();
|
||||||
synchronized(_currentCookieMap!!) {
|
synchronized(_currentCookieMap!!) {
|
||||||
for(cookie in _currentCookieMap!!
|
for(cookie in _currentCookieMap!!
|
||||||
.filter { domain.matchesDomain(it.key) }
|
.filter { domain.matchesDomain(it.key) }
|
||||||
.flatMap { it.value.toList() })
|
.flatMap { it.value.toList() })
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
};
|
};
|
||||||
|
|
||||||
if(cookiesToApply.size > 0) {
|
if(cookiesToApply.size > 0) {
|
||||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||||
request.headers["Cookie"] = cookieString;
|
|
||||||
}
|
val existingCookies = request.headers["Cookie"];
|
||||||
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
if(!existingCookies.isNullOrEmpty())
|
||||||
|
newBuilder?.header("Cookie", existingCookies.trim(';') + "; " + cookieString);
|
||||||
|
else
|
||||||
|
newBuilder?.header("Cookie", cookieString);
|
||||||
}
|
}
|
||||||
|
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jsClient?.validateUrlOrThrow(request.url);
|
_jsClient?.validateUrlOrThrow(request.url.toString());
|
||||||
super.beforeRequest(request)
|
return newBuilder?.let { it.build() } ?: request;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterRequest(request: Request, resp: Response) {
|
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||||
super.afterRequest(request, resp)
|
|
||||||
|
|
||||||
if(doUpdateCookies) {
|
if(doUpdateCookies) {
|
||||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
val domain = resp.request.url.host.lowercase();
|
||||||
val domainParts = domain!!.split(".");
|
val domainParts = domain.split(".");
|
||||||
val defaultCookieDomain =
|
val defaultCookieDomain =
|
||||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
for (header in resp.headers) {
|
for (header in resp.headers) {
|
||||||
if (_currentCookieMap != null && header.key.lowercase() == "set-cookie") {
|
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
|
||||||
val newCookies = cookieStringToMap(header.value);
|
//val newCookies = cookieStringToMap(header.second.split("; "));
|
||||||
for (cookie in newCookies) {
|
val cookie = cookieStringToPair(header.second);
|
||||||
val endIndex = cookie.value.indexOf(";");
|
//for (cookie in newCookies) {
|
||||||
var cookieValue = cookie.value;
|
var cookieValue = cookie.second;
|
||||||
var domainToUse = domain;
|
var domainToUse = domain;
|
||||||
|
|
||||||
if (endIndex > 0) {
|
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) {
|
||||||
val cookieParts = cookie.value.split(";");
|
val cookieParts = cookie.second.split(";");
|
||||||
if (cookieParts.size == 0)
|
if (cookieParts.size == 0)
|
||||||
continue;
|
continue;
|
||||||
cookieValue = cookieParts[0].trim();
|
cookieValue = cookieParts[0].trim();
|
||||||
@@ -114,24 +132,29 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
_currentCookieMap!!.put(domainToUse, newMap)
|
_currentCookieMap!!.put(domainToUse, newMap)
|
||||||
newMap;
|
newMap;
|
||||||
}
|
}
|
||||||
if(cookieMap.containsKey(cookie.key) || doAllowNewCookies)
|
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
cookieMap.put(cookie.key, cookieValue);
|
cookieMap.put(cookie.first, cookieValue);
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
||||||
val map = hashMapOf<String, String>();
|
val map = hashMapOf<String, String>();
|
||||||
for(cookie in parts) {
|
for(cookie in parts) {
|
||||||
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
val pair = cookieStringToPair(cookie)
|
||||||
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
map.put(pair.first, pair.second);
|
||||||
map.put(cookieKey.trim(), cookieVal.trim());
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
||||||
|
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
||||||
|
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
||||||
|
return Pair(cookieKey.trim(), cookieVal.trim());
|
||||||
|
}
|
||||||
|
|
||||||
//Prints out code for test reproduction..
|
//Prints out code for test reproduction..
|
||||||
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
||||||
@@ -155,4 +178,5 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
Logger.i("Testing", code);
|
Logger.i("Testing", code);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,6 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||||
Logger.i("JSContent", "name=$name");
|
|
||||||
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
||||||
|
|
||||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.futo.platformplayer.BuildConfig
|
|||||||
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.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.warnIfMainThread
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
this.pager = pager;
|
this.pager = pager;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
getResults();
|
getResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||||
pager.invoke("nextPage", arrayOf<Any>());
|
pager.invoke("nextPage", arrayOf<Any>());
|
||||||
};
|
};
|
||||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
_resultChanged = true;
|
_resultChanged = true;
|
||||||
/*
|
/*
|
||||||
try {
|
try {
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueNull
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
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
|
||||||
@@ -99,8 +100,11 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
return getCommentsJS(client);
|
return getCommentsJS(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
|
||||||
|
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||||
|
return null;
|
||||||
|
|
||||||
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -9,7 +9,7 @@ import com.futo.platformplayer.getOrNull
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
|
|
||||||
class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSource {
|
class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||||
override val codec: String = "HLS";
|
override val codec: String = "HLS";
|
||||||
override val name : String;
|
override val name : String;
|
||||||
@@ -31,9 +31,6 @@ class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSou
|
|||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl(): String {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
||||||
|
|||||||
+1
-5
@@ -7,7 +7,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||||
override val width : Int = 0;
|
override val width : Int = 0;
|
||||||
override val height : Int = 0;
|
override val height : Int = 0;
|
||||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||||
@@ -28,8 +28,4 @@ class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
|||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVideoUrl(): String {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,8 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
|
|||||||
_currentResults = dedupResults(_basePager.getResults());
|
_currentResults = dedupResults(_basePager.getResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasMorePages(): Boolean = _basePager.hasMorePages();
|
override fun hasMorePages(): Boolean =
|
||||||
|
_basePager.hasMorePages();
|
||||||
override fun nextPage() {
|
override fun nextPage() {
|
||||||
_basePager.nextPage()
|
_basePager.nextPage()
|
||||||
_currentResults = dedupResults(_basePager.getResults());
|
_currentResults = dedupResults(_basePager.getResults());
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import java.util.stream.IntStream
|
|||||||
* A PlatformContent MultiPager that orders the results of a page based on the datetime of a content item
|
* A PlatformContent MultiPager that orders the results of a page based on the datetime of a content item
|
||||||
*/
|
*/
|
||||||
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
||||||
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false) : super(pagers.map { it }.toList(), allowFailure) {}
|
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers.map { it }.toList(), allowFailure, pageSize) {}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ abstract class MultiPager<T> : IPager<T> {
|
|||||||
protected val _subSinglePagers : MutableList<SingleItemPager<T>>;
|
protected val _subSinglePagers : MutableList<SingleItemPager<T>>;
|
||||||
protected val _failedPagers: ArrayList<IPager<T>> = arrayListOf();
|
protected val _failedPagers: ArrayList<IPager<T>> = arrayListOf();
|
||||||
|
|
||||||
private val _pageSize : Int = 9;
|
private var _pageSize : Int = 9;
|
||||||
|
|
||||||
private var _didInitialize = false;
|
private var _didInitialize = false;
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ abstract class MultiPager<T> : IPager<T> {
|
|||||||
|
|
||||||
val totalPagers: Int get() = _pagers.size;
|
val totalPagers: Int get() = _pagers.size;
|
||||||
|
|
||||||
constructor(pagers : List<IPager<T>>, allowFailure: Boolean = false) {
|
constructor(pagers : List<IPager<T>>, allowFailure: Boolean = false, pageSize: Int = 9) {
|
||||||
|
this._pageSize = pageSize;
|
||||||
this.allowFailure = allowFailure;
|
this.allowFailure = allowFailure;
|
||||||
_pagers = pagers.toMutableList();
|
_pagers = pagers.toMutableList();
|
||||||
_subSinglePagers = _pagers.map { SingleItemPager(it) }.toMutableList();
|
_subSinglePagers = _pagers.map { SingleItemPager(it) }.toMutableList();
|
||||||
|
|||||||
+2
-2
@@ -137,11 +137,11 @@ abstract class MultiParallelPager<T> : IPager<T>, IAsyncPager<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Pager prepare in ${timeForPage}ms");
|
Logger.v(TAG, "Pager prepare in ${timeForPage}ms");
|
||||||
val timeAwait = measureTimeMillis {
|
val timeAwait = measureTimeMillis {
|
||||||
_currentResults = results.map { it.await() }.mapNotNull { it };
|
_currentResults = results.map { it.await() }.mapNotNull { it };
|
||||||
};
|
};
|
||||||
Logger.i(TAG, "Pager load in ${timeAwait}ms");
|
Logger.v(TAG, "Pager load in ${timeAwait}ms");
|
||||||
|
|
||||||
_currentResultExceptions = exceptions;
|
_currentResultExceptions = exceptions;
|
||||||
return _currentResults;
|
return _currentResults;
|
||||||
|
|||||||
+24
-3
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.structures
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -37,8 +38,12 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
|||||||
synchronized(_pending) {
|
synchronized(_pending) {
|
||||||
_pending.remove(pendingPager);
|
_pending.remove(pendingPager);
|
||||||
}
|
}
|
||||||
if(error != null)
|
if(error != null) {
|
||||||
onPagerError.emit(error);
|
onPagerError.emit(error);
|
||||||
|
val replacing = _placeHolderPagersPaired[pendingPager];
|
||||||
|
if(replacing != null)
|
||||||
|
updatePager(null, replacing, error);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
updatePager(pendingPager.getCompleted());
|
updatePager(pendingPager.getCompleted());
|
||||||
}
|
}
|
||||||
@@ -60,9 +65,25 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
|||||||
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
|
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
|
||||||
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
||||||
|
|
||||||
private fun updatePager(pagerToAdd: IPager<T>?) {
|
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
||||||
if(pagerToAdd == null)
|
if(pagerToAdd == null) {
|
||||||
|
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||||
|
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||||
|
|
||||||
|
_pagersReusable.add((PlaceholderPager(5) {
|
||||||
|
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||||
|
} as IPager<T>).asReusable());
|
||||||
|
_currentPager = recreatePager(getCurrentSubPagers());
|
||||||
|
|
||||||
|
if(_currentPager is MultiParallelPager<*>)
|
||||||
|
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
|
||||||
|
else if(_currentPager is MultiPager<*>)
|
||||||
|
(_currentPager as MultiPager).initialize()
|
||||||
|
|
||||||
|
onPagerChanged.emit(_currentPager);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
synchronized(_pagersReusable) {
|
synchronized(_pagersReusable) {
|
||||||
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
||||||
_pagersReusable.add(pagerToAdd.asReusable());
|
_pagersReusable.add(pagerToAdd.asReusable());
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
* A placeholder pager simply generates PlatformContent by some creator function.
|
* A placeholder pager simply generates PlatformContent by some creator function.
|
||||||
*/
|
*/
|
||||||
class PlaceholderPager : IPager<IPlatformContent> {
|
class PlaceholderPager : IPager<IPlatformContent> {
|
||||||
private val _creator: ()->IPlatformContent;
|
val placeholderFactory: ()->IPlatformContent;
|
||||||
private val _pageSize: Int;
|
private val _pageSize: Int;
|
||||||
|
|
||||||
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
|
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
|
||||||
_creator = placeholderCreator;
|
placeholderFactory = placeholderCreator;
|
||||||
_pageSize = pageSize;
|
_pageSize = pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class PlaceholderPager : IPager<IPlatformContent> {
|
|||||||
override fun getResults(): List<IPlatformContent> {
|
override fun getResults(): List<IPlatformContent> {
|
||||||
val pages = ArrayList<IPlatformContent>();
|
val pages = ArrayList<IPlatformContent>();
|
||||||
for(item in 1.._pageSize)
|
for(item in 1.._pageSize)
|
||||||
pages.add(_creator());
|
pages.add(placeholderFactory());
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
override fun hasMorePages(): Boolean = true;
|
override fun hasMorePages(): Boolean = true;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class ChannelContentCache {
|
|||||||
init {
|
init {
|
||||||
val results = pager.getResults();
|
val results = pager.getResults();
|
||||||
|
|
||||||
Logger.i(TAG, "Caching ${results.size} subscription initial results");
|
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val newCacheItems = instance.cacheVideos(results);
|
val newCacheItems = instance.cacheVideos(results);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
override fun serviceResolved(event: ServiceEvent) {
|
||||||
Logger.i(TAG, "ChromeCast service resolved: " + event.info);
|
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
||||||
addOrUpdateDevice(event);
|
addOrUpdateDevice(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
|||||||
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
|
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
|
||||||
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
|
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
|
||||||
|
|
||||||
|
fun hasListeners(): Boolean =
|
||||||
|
synchronized(_listeners){_listeners.isNotEmpty()} ||
|
||||||
|
synchronized(_conditionalListeners){_conditionalListeners.isNotEmpty()};
|
||||||
|
|
||||||
fun subscribeConditional(listener: ConditionalHandler) {
|
fun subscribeConditional(listener: ConditionalHandler) {
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
_conditionalListeners.add(TaggedHandler(listener));
|
_conditionalListeners.add(TaggedHandler(listener));
|
||||||
@@ -65,10 +69,7 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
|||||||
|
|
||||||
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
||||||
fun emit() : Boolean {
|
fun emit() : Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -76,6 +77,7 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke();
|
handler.handler.invoke();
|
||||||
}
|
}
|
||||||
@@ -85,17 +87,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
}
|
}
|
||||||
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
||||||
fun emit(value : T1): Boolean {
|
fun emit(value : T1): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
handled = handled || conditional.handler.invoke(value);
|
handled = handled || conditional.handler.invoke(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value);
|
handler.handler.invoke(value);
|
||||||
}
|
}
|
||||||
@@ -105,10 +104,7 @@ class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
|||||||
}
|
}
|
||||||
class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
||||||
fun emit(value1 : T1, value2 : T2): Boolean {
|
fun emit(value1 : T1, value2 : T2): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -116,6 +112,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value1, value2);
|
handler.handler.invoke(value1, value2);
|
||||||
}
|
}
|
||||||
@@ -126,10 +123,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
|
|
||||||
class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() {
|
class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() {
|
||||||
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -137,6 +131,7 @@ class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value1, value2, value3);
|
handler.handler.invoke(value1, value2, value3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class TaskHandler<TParameter, TResult> {
|
|||||||
fun run(parameter: TParameter) {
|
fun run(parameter: TParameter) {
|
||||||
val id = ++_idGenerator;
|
val id = ++_idGenerator;
|
||||||
|
|
||||||
|
var handled = false;
|
||||||
_scope().launch(_dispatcher) {
|
_scope().launch(_dispatcher) {
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator)
|
||||||
return@launch;
|
return@launch;
|
||||||
@@ -67,35 +68,53 @@ class TaskHandler<TParameter, TResult> {
|
|||||||
return@launch;
|
return@launch;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator) {
|
||||||
|
handled = true;
|
||||||
return@withContext;
|
return@withContext;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onSuccess.emit(result);
|
onSuccess.emit(result);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
|
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
|
||||||
onError.emit(e, parameter);
|
onError.emit(e, parameter);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
|
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator) {
|
||||||
|
handled = true;
|
||||||
return@launch;
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
handled = true;
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator)
|
||||||
return@withContext;
|
return@withContext;
|
||||||
|
|
||||||
if (!onError.emit(e, parameter)) {
|
if (!onError.emit(e, parameter)) {
|
||||||
Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e);
|
Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e);
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Handled exception in TaskHandler invoke.", e);
|
//Logger.w(TAG, "Handled exception in TaskHandler invoke.", e); (Prevents duplicate logs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway
|
||||||
|
if(!handled) {
|
||||||
|
if(it is CancellationException) {
|
||||||
|
Logger.w(TAG, "Detected unhandled TaskHandler due to cancellation, forwarding cancellation");
|
||||||
|
onError.emit(it, parameter);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//TODO: Forward exception?
|
||||||
|
Logger.w(TAG, "Detected unhandled TaskHandler due to [${it}]", it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.developer
|
package com.futo.platformplayer.developer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.LoginActivity
|
import com.futo.platformplayer.activities.LoginActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
@@ -201,6 +202,28 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@HttpPOST("/plugin/captchaTestPlugin")
|
||||||
|
fun pluginCaptchaTestPlugin(context: HttpContext) {
|
||||||
|
val config = _testPlugin?.config as SourcePluginConfig;
|
||||||
|
val url = context.query.get("url")
|
||||||
|
val html = context.readContentString();
|
||||||
|
try {
|
||||||
|
val captchaConfig = config.captcha;
|
||||||
|
if(captchaConfig == null) {
|
||||||
|
context.respondCode(403, "This plugin doesn't support captcha");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CaptchaActivity.showCaptcha(StateApp.instance.context, config, url, html) {
|
||||||
|
_testPluginVariables.clear();
|
||||||
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, it), JSHttpClient(null, null, it));
|
||||||
|
|
||||||
|
};
|
||||||
|
context.respondCode(200, "Captcha started");
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
@HttpGET("/plugin/loginTestPlugin")
|
@HttpGET("/plugin/loginTestPlugin")
|
||||||
fun pluginLoginTestPlugin(context: HttpContext) {
|
fun pluginLoginTestPlugin(context: HttpContext) {
|
||||||
val config = _testPlugin?.config as SourcePluginConfig;
|
val config = _testPlugin?.config as SourcePluginConfig;
|
||||||
@@ -416,7 +439,7 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val resp = _client.get(body.url!!, body.headers);
|
val resp = _client.get(body.url!!, body.headers);
|
||||||
|
|
||||||
context.respondCode(200,
|
context.respondCode(200,
|
||||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.code, resp.body?.string())),
|
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
||||||
context.query.getOrDefault("CT", "text/plain"));
|
context.query.getOrDefault("CT", "text/plain"));
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
@@ -58,13 +59,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
clearFocus();
|
clearFocus();
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|
||||||
Logger.i(TAG, "Set AutoBackupPassword");
|
Logger.i(TAG, "Set AutoBackupPassword");
|
||||||
Settings.instance.backup.autoBackupPassword = _editPassword.text.toString();
|
Settings.instance.backup.autoBackupPassword = _editPassword.text.toString();
|
||||||
Settings.instance.backup.didAskAutoBackup = true;
|
Settings.instance.backup.didAskAutoBackup = true;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
|
|
||||||
UIDialogs.toast(context, "AutoBackup enabled");
|
UIDialogs.toast(context, "AutoBackup enabled");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateBackup.startAutomaticBackup(true);
|
StateBackup.startAutomaticBackup(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ package com.futo.platformplayer.dialogs
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
@@ -32,6 +36,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
private lateinit var _buttonCancel: MaterialButton;
|
private lateinit var _buttonCancel: MaterialButton;
|
||||||
private lateinit var _editComment: EditText;
|
private lateinit var _editComment: EditText;
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
private lateinit var _inputMethodManager: InputMethodManager;
|
||||||
|
private lateinit var _textCharacterCount: TextView;
|
||||||
|
private lateinit var _textCharacterCountMax: TextView;
|
||||||
|
|
||||||
val onCommentAdded = Event1<IPlatformComment>();
|
val onCommentAdded = Event1<IPlatformComment>();
|
||||||
|
|
||||||
@@ -42,6 +48,26 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_buttonCancel = findViewById(R.id.button_cancel);
|
_buttonCancel = findViewById(R.id.button_cancel);
|
||||||
_buttonCreate = findViewById(R.id.button_create);
|
_buttonCreate = findViewById(R.id.button_create);
|
||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
|
||||||
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
|
_textCharacterCount.setTextColor(Color.RED);
|
||||||
|
_textCharacterCountMax.setTextColor(Color.RED);
|
||||||
|
_buttonCreate.alpha = 0.4f;
|
||||||
|
} else {
|
||||||
|
_textCharacterCount.setTextColor(Color.WHITE);
|
||||||
|
_textCharacterCountMax.setTextColor(Color.WHITE);
|
||||||
|
_buttonCreate.alpha = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
@@ -53,11 +79,16 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
|
if (_editComment.text.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
|
UIDialogs.toast(context, "Comment should be less than 5000 characters");
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
val comment = _editComment.text.toString();
|
val comment = _editComment.text.toString();
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!
|
val processHandle = StatePolycentric.instance.processHandle!!
|
||||||
val eventPointer = processHandle.post(comment, null, ref)
|
val eventPointer = processHandle.post(comment, null, ref)
|
||||||
|
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillServers()
|
processHandle.fullyBackfillServers()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
package com.futo.platformplayer.downloads
|
package com.futo.platformplayer.downloads
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class PlaylistDownloadDescriptor(
|
data class PlaylistDownloadDescriptor(
|
||||||
val id: String,
|
val id: String,
|
||||||
val targetPxCount: Long?,
|
val targetPxCount: Long?,
|
||||||
val targetBitrate: Long?
|
val targetBitrate: Long?
|
||||||
);
|
) {
|
||||||
|
var preventDownload: MutableList<String> = arrayListOf();
|
||||||
|
|
||||||
|
fun getPreventDownloadList(): List<String> = synchronized(preventDownload){ preventDownload };
|
||||||
|
fun shouldDownload(video: IPlatformVideo): Boolean {
|
||||||
|
synchronized(preventDownload) {
|
||||||
|
return !preventDownload.contains(video.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,16 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
|
import com.futo.platformplayer.hasAnySource
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.isDownloadable
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
@@ -27,7 +31,6 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
@@ -147,27 +150,37 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
|
}
|
||||||
|
|
||||||
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
||||||
if(videoSource == null && targetPixelCount != null) {
|
if(videoSource == null && targetPixelCount != null) {
|
||||||
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
||||||
?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
if(vsource is IVideoUrlSource)
|
if(vsource != null) {
|
||||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
if (vsource is IVideoUrlSource)
|
||||||
else
|
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||||
throw IllegalStateException("Download video source is not a url source");
|
else
|
||||||
|
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(audioSource == null && targetBitrate != null) {
|
if(audioSource == null && targetBitrate != null) {
|
||||||
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw IllegalStateException("Could not find a valid audio source for video");
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
if(asource == null)
|
if(asource == null)
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
else if(asource is IAudioUrlSource)
|
else if(asource is IAudioUrlSource)
|
||||||
audioSource = AudioUrlSource.fromUrlSource(asource);
|
audioSource = AudioUrlSource.fromUrlSource(asource);
|
||||||
else
|
else
|
||||||
throw IllegalStateException("Download audio source is not a url source");
|
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(videoSource == null && audioSource == null)
|
||||||
|
throw DownloadException("No valid sources found for video/audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||||
@@ -358,7 +371,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled)
|
if (isCancelled)
|
||||||
throw IllegalStateException("Cancelled");
|
throw CancellationException("Cancelled");
|
||||||
} while (read > 0);
|
} while (read > 0);
|
||||||
|
|
||||||
lastSpeed = 0;
|
lastSpeed = 0;
|
||||||
@@ -410,7 +423,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(isCancelled)
|
if(isCancelled)
|
||||||
throw IllegalStateException("Cancelled");
|
throw CancellationException("Cancelled", null);
|
||||||
}
|
}
|
||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.futo.platformplayer.engine
|
package com.futo.platformplayer.engine
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Looper
|
|
||||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
@@ -18,9 +17,7 @@ import com.futo.platformplayer.engine.exceptions.*
|
|||||||
import com.futo.platformplayer.engine.internal.V8Converter
|
import com.futo.platformplayer.engine.internal.V8Converter
|
||||||
import com.futo.platformplayer.engine.packages.*
|
import com.futo.platformplayer.engine.packages.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
class V8Plugin {
|
class V8Plugin {
|
||||||
val config: IV8PluginConfig;
|
val config: IV8PluginConfig;
|
||||||
@@ -31,14 +28,31 @@ class V8Plugin {
|
|||||||
val httpClient: ManagedHttpClient get() = _client;
|
val httpClient: ManagedHttpClient get() = _client;
|
||||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||||
|
|
||||||
|
private val _runtimeLock = Object();
|
||||||
var _runtime : V8Runtime? = null;
|
var _runtime : V8Runtime? = null;
|
||||||
|
|
||||||
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
|
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
|
||||||
private val _depsPackages : MutableList<V8Package> = mutableListOf();
|
private val _depsPackages : MutableList<V8Package> = mutableListOf();
|
||||||
private var _script : String? = null;
|
private var _script : String? = null;
|
||||||
|
|
||||||
|
var isStopped = true;
|
||||||
val onStopped = Event1<V8Plugin>();
|
val onStopped = Event1<V8Plugin>();
|
||||||
|
|
||||||
|
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
|
||||||
|
private val _busyCounterLock = Object();
|
||||||
|
private var _busyCounter = 0;
|
||||||
|
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before a busy counter is about to be removed.
|
||||||
|
* Is primarily used to prevent additional calls to dead runtimes.
|
||||||
|
*
|
||||||
|
* Parameter is the busy count after this execution
|
||||||
|
*/
|
||||||
|
val afterBusy = Event1<Int>();
|
||||||
|
|
||||||
|
val onScriptException = Event1<ScriptException>();
|
||||||
|
|
||||||
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
|
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._clientAuth = clientAuth;
|
this._clientAuth = clientAuth;
|
||||||
@@ -81,7 +95,7 @@ class V8Plugin {
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
||||||
synchronized(this) {
|
synchronized(_runtimeLock) {
|
||||||
if (_runtime != null)
|
if (_runtime != null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -121,19 +135,25 @@ class V8Plugin {
|
|||||||
catchScriptErrors("Plugin[${config.name}]") {
|
catchScriptErrors("Plugin[${config.name}]") {
|
||||||
it.getExecutor(script).executeVoid()
|
it.getExecutor(script).executeVoid()
|
||||||
};
|
};
|
||||||
|
isStopped = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun stop(){
|
fun stop(){
|
||||||
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
||||||
synchronized(this) {
|
isStopped = true;
|
||||||
_runtime?.let {
|
whenNotBusy {
|
||||||
_runtime = null;
|
synchronized(_runtimeLock) {
|
||||||
if(!it.isClosed && !it.isDead)
|
isStopped = true;
|
||||||
it.close();
|
_runtime?.let {
|
||||||
};
|
_runtime = null;
|
||||||
|
if(!it.isClosed && !it.isDead)
|
||||||
|
it.close();
|
||||||
|
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
onStopped.emit(this);
|
||||||
}
|
}
|
||||||
onStopped.emit(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun execute(js: String) : V8Value {
|
fun execute(js: String) : V8Value {
|
||||||
@@ -141,14 +161,53 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
fun <T : V8Value> executeTyped(js: String) : T {
|
fun <T : V8Value> executeTyped(js: String) : T {
|
||||||
warnIfMainThread("V8Plugin.executeTyped");
|
warnIfMainThread("V8Plugin.executeTyped");
|
||||||
|
if(isStopped)
|
||||||
|
throw PluginEngineStoppedException(config, "Instance is stopped", js);
|
||||||
|
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
_busyCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
||||||
return catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() };
|
try {
|
||||||
|
return catchScriptErrors("Plugin[${config.name}]", js) {
|
||||||
|
runtime.getExecutor(js).execute()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
|
||||||
|
try {
|
||||||
|
afterBusy.emit(_busyCounter - 1);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
|
||||||
|
}
|
||||||
|
_busyCounter--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
|
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
|
||||||
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
|
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
|
||||||
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
|
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
|
||||||
|
|
||||||
|
fun whenNotBusy(handler: (V8Plugin)->Unit) {
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
if(_busyCounter == 0)
|
||||||
|
handler(this);
|
||||||
|
else {
|
||||||
|
val tag = Object();
|
||||||
|
afterBusy.subscribe(tag) {
|
||||||
|
if(it == 0) {
|
||||||
|
Logger.w(TAG, "V8Plugin afterBusy handled");
|
||||||
|
afterBusy.remove(tag);
|
||||||
|
handler(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPackage(context: Context, packageName: String): V8Package {
|
private fun getPackage(context: Context, packageName: String): V8Package {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
@@ -160,7 +219,13 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T {
|
fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T {
|
||||||
return catchScriptErrors(this.config, context, code, handle);
|
try {
|
||||||
|
return catchScriptErrors(this.config, context, code, handle);
|
||||||
|
}
|
||||||
|
catch(ex: ScriptException) {
|
||||||
|
onScriptException.emit(ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -185,7 +250,7 @@ class V8Plugin {
|
|||||||
if(result is V8ValueObject) {
|
if(result is V8ValueObject) {
|
||||||
val type = result.getString("plugin_type");
|
val type = result.getString("plugin_type");
|
||||||
if(type != null && type.endsWith("Exception"))
|
if(type != null && type.endsWith("Exception"))
|
||||||
Companion.throwExceptionFromV8(
|
throwExceptionFromV8(
|
||||||
config,
|
config,
|
||||||
result.getOrThrow(config, "plugin_type", "V8Plugin"),
|
result.getOrThrow(config, "plugin_type", "V8Plugin"),
|
||||||
result.getOrThrow(config, "message", "V8Plugin"),
|
result.getOrThrow(config, "message", "V8Plugin"),
|
||||||
@@ -202,19 +267,28 @@ class V8Plugin {
|
|||||||
throw ScriptCompilationException(config, "Compilation: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
throw ScriptCompilationException(config, "Compilation: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
||||||
}
|
}
|
||||||
catch(executeEx: JavetExecutionException) {
|
catch(executeEx: JavetExecutionException) {
|
||||||
val exMessage = extractJSExceptionMessage(executeEx);
|
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
|
||||||
|
val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
|
||||||
|
|
||||||
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true)
|
//Captcha
|
||||||
|
if (pluginType == "CaptchaRequiredException") {
|
||||||
|
throw ScriptCaptchaRequiredException(config,
|
||||||
|
executeEx.scriptingError.context["url"]?.toString(),
|
||||||
|
executeEx.scriptingError.context["body"]?.toString(),
|
||||||
|
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Others
|
||||||
throwExceptionFromV8(
|
throwExceptionFromV8(
|
||||||
config,
|
config,
|
||||||
executeEx.scriptingError.context["plugin_type"].toString(),
|
pluginType,
|
||||||
(exMessage ?: ""),
|
(extractJSExceptionMessage(executeEx) ?: ""),
|
||||||
executeEx,
|
executeEx,
|
||||||
executeEx.scriptingError?.stack,
|
executeEx.scriptingError?.stack,
|
||||||
codeStripped
|
codeStripped
|
||||||
);
|
);
|
||||||
|
}
|
||||||
throw ScriptExecutionException(config, "${exMessage}", null, executeEx.scriptingError?.stack, codeStripped);
|
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -224,6 +298,7 @@ class V8Plugin {
|
|||||||
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
||||||
when(pluginType) {
|
when(pluginType) {
|
||||||
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
||||||
|
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
||||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
|
||||||
|
open class PluginEngineException(config: IV8PluginConfig, error: String, code: String? = null) : PluginException(config, error, null, code) {
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
|
||||||
|
class PluginEngineStoppedException(config: IV8PluginConfig, error: String, code: String? = null) : PluginEngineException(config, error, code) {
|
||||||
|
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?, val body: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, "Captcha required", ex, stack, code) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||||
|
val contextName = "ScriptCaptchaRequiredException";
|
||||||
|
return ScriptCaptchaRequiredException(config,
|
||||||
|
obj.getOrDefault<String>(config, "url", contextName, null),
|
||||||
|
obj.getOrDefault<String>(config, "body", contextName, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||||
|
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,11 +108,12 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class BridgeHttpResponse(val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
||||||
val isOk = code >= 200 && code < 300;
|
val isOk = code >= 200 && code < 300;
|
||||||
|
|
||||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
val obj = runtime.createV8ValueObject();
|
val obj = runtime.createV8ValueObject();
|
||||||
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
@@ -227,7 +228,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse(method, url, resp.code, resp.headers, responseBody);
|
logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -241,7 +242,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse(method, url, resp.code, resp.headers, responseBody);
|
logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,7 +257,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse("GET", url, resp.code, resp.headers, responseBody);
|
logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -270,7 +271,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse("POST", url, resp.code, resp.headers, responseBody);
|
logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -367,7 +368,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse(408, null);
|
return BridgeHttpResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +462,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse(408, null);
|
return BridgeHttpResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.futo.platformplayer.exceptions
|
||||||
|
|
||||||
|
class DownloadException : Throwable {
|
||||||
|
val isRetryable: Boolean;
|
||||||
|
|
||||||
|
constructor(innerException: Throwable, retryable: Boolean = true): super(innerException) {
|
||||||
|
isRetryable = retryable;
|
||||||
|
}
|
||||||
|
constructor(msg: String, retryable: Boolean = true): super(msg) {
|
||||||
|
isRetryable = retryable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.futo.platformplayer.exceptions
|
||||||
|
|
||||||
|
class RateLimitException : Throwable {
|
||||||
|
val pluginIds: List<String>;
|
||||||
|
|
||||||
|
constructor(pluginIds: List<String>): super() {
|
||||||
|
this.pluginIds = pluginIds ?: listOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
};
|
};
|
||||||
_textName?.text = channel.name;
|
_textName?.text = channel.name;
|
||||||
|
|
||||||
val metadata = "${channel.subscribers.toHumanNumber()} subscribers";
|
val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||||
_textMetadata?.text = metadata;
|
_textMetadata?.text = metadata;
|
||||||
_lastChannel = channel;
|
_lastChannel = channel;
|
||||||
setLinks(channel.links, channel.name);
|
setLinks(channel.links, channel.name);
|
||||||
|
|||||||
+4
-1
@@ -29,6 +29,7 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
@@ -76,7 +77,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
}).success {
|
}).success {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(it);
|
setPager(it);
|
||||||
}.exception<Throwable> {
|
}
|
||||||
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load initial videos.", it);
|
Logger.w(TAG, "Failed to load initial videos.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-1
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -52,7 +53,8 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
|
|||||||
_authorLinks.add(PlatformAuthorLink(it.id, it.name, it.url, it.thumbnail));
|
_authorLinks.add(PlatformAuthorLink(it.id, it.name, it.url, it.thumbnail));
|
||||||
adapter.notifyItemInserted(adapter.childToParentPosition(_authorLinks.size - 1));
|
adapter.notifyItemInserted(adapter.childToParentPosition(_authorLinks.size - 1));
|
||||||
loadNext();
|
loadNext();
|
||||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
}.exception<ScriptCaptchaRequiredException> { }
|
||||||
|
.exceptionWithParameter<Throwable> { ex, para ->
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||||
UIDialogs.toast(requireContext(), "Failed to fetch\n${para}", false)
|
UIDialogs.toast(requireContext(), "Failed to fetch\n${para}", false)
|
||||||
loadNext();
|
loadNext();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.futopay.PaymentConfigurations
|
import com.futo.futopay.PaymentConfigurations
|
||||||
import com.futo.futopay.PaymentManager
|
import com.futo.futopay.PaymentManager
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -68,9 +69,12 @@ class BuyFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonBuy.setOnClickListener {
|
if(!BuildConfig.IS_PLAYSTORE_BUILD)
|
||||||
buy();
|
_buttonBuy.setOnClickListener {
|
||||||
}
|
buy();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_buttonBuy.visibility = View.GONE;
|
||||||
_buttonPaid.setOnClickListener {
|
_buttonPaid.setOnClickListener {
|
||||||
paid();
|
paid();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -361,7 +361,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(channel);
|
_buttonSubscribe.setSubscribeChannel(channel);
|
||||||
_textChannel.text = channel.name;
|
_textChannel.text = channel.name;
|
||||||
_textChannelSub.text = "${channel.subscribers.toHumanNumber()} subscribers";
|
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
||||||
Glide.with(_imageBanner)
|
Glide.with(_imageBanner)
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -86,7 +87,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||||
|
|||||||
+2
@@ -13,6 +13,7 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
|||||||
constructor(fragment: CreatorSearchResultsFragment, inflater: LayoutInflater): super(fragment, inflater) {
|
constructor(fragment: CreatorSearchResultsFragment, inflater: LayoutInflater): super(fragment, inflater) {
|
||||||
_taskSearch = TaskHandler<String, IPager<PlatformAuthorLink>>({fragment.lifecycleScope}, { query -> StatePlatform.instance.searchChannels(query) })
|
_taskSearch = TaskHandler<String, IPager<PlatformAuthorLink>>({fragment.lifecycleScope}, { query -> StatePlatform.instance.searchChannels(query) })
|
||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private val _spinnerSortBy: Spinner;
|
private val _spinnerSortBy: Spinner;
|
||||||
private val _containerSortBy: LinearLayout;
|
private val _containerSortBy: LinearLayout;
|
||||||
private val _tagsView: TagsView;
|
private val _tagsView: TagsView;
|
||||||
|
private val _textCentered: TextView;
|
||||||
|
|
||||||
protected val _toolbarContentView: LinearLayout;
|
protected val _toolbarContentView: LinearLayout;
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_feed, this);
|
inflater.inflate(R.layout.fragment_feed, this);
|
||||||
|
|
||||||
|
_textCentered = findViewById(R.id.text_centered);
|
||||||
_progress_bar = findViewById(R.id.progress_bar);
|
_progress_bar = findViewById(R.id.progress_bar);
|
||||||
_progress_bar.inactiveColor = Color.TRANSPARENT;
|
_progress_bar.inactiveColor = Color.TRANSPARENT;
|
||||||
|
|
||||||
@@ -169,6 +171,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
_recyclerResults.addOnScrollListener(_scrollListener);
|
_recyclerResults.addOnScrollListener(_scrollListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun setTextCentered(text: String?) {
|
||||||
|
_textCentered.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
//Reload the pager if the plugin was killed
|
//Reload the pager if the plugin was killed
|
||||||
val pager = recyclerData.pager;
|
val pager = recyclerData.pager;
|
||||||
|
|||||||
+15
-3
@@ -8,21 +8,27 @@ import android.view.ViewGroup
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
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.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
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.others.CaptchaWebViewClient
|
||||||
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.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
@@ -92,6 +98,7 @@ class HomeFragment : MainFragment() {
|
|||||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<ScriptExecutionException> {
|
.exception<ScriptExecutionException> {
|
||||||
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
||||||
@@ -100,17 +107,20 @@ class HomeFragment : MainFragment() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
.exception<ScriptImplementationException> {
|
.exception<ScriptImplementationException> {
|
||||||
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
Logger.w(TAG, "Plugin failure.", it);
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
||||||
UIDialogs.Action("Ignore", {}),
|
UIDialogs.Action("Ignore", {}),
|
||||||
UIDialogs.Action("Sources", { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
UIDialogs.Action("Sources", { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, {
|
||||||
loadResults()
|
loadResults()
|
||||||
});
|
}) {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +141,8 @@ class HomeFragment : MainFragment() {
|
|||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
|
|||||||
+15
@@ -69,6 +69,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
private var _currentLoadIndex = 0;
|
private var _currentLoadIndex = 0;
|
||||||
|
|
||||||
private var _taskLoadChannel: TaskHandler<String, IPlatformChannel>;
|
private var _taskLoadChannel: TaskHandler<String, IPlatformChannel>;
|
||||||
|
private var _counter: Int = 0;
|
||||||
|
private var _limitToastShown = false;
|
||||||
|
|
||||||
constructor(fragment: ImportSubscriptionsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: ImportSubscriptionsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
@@ -104,6 +106,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
_taskLoadChannel = TaskHandler<String, IPlatformChannel>({_fragment.lifecycleScope}, { link ->
|
_taskLoadChannel = TaskHandler<String, IPlatformChannel>({_fragment.lifecycleScope}, { link ->
|
||||||
|
_counter++;
|
||||||
val channel: IPlatformChannel = StatePlatform.instance.getChannelLive(link, false);
|
val channel: IPlatformChannel = StatePlatform.instance.getChannelLive(link, false);
|
||||||
return@TaskHandler channel;
|
return@TaskHandler channel;
|
||||||
}).success {
|
}).success {
|
||||||
@@ -124,6 +127,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onShown(parameter: Any ?, isBack: Boolean) {
|
fun onShown(parameter: Any ?, isBack: Boolean) {
|
||||||
|
_counter = 0;
|
||||||
|
_limitToastShown = false;
|
||||||
updateSelected();
|
updateSelected();
|
||||||
|
|
||||||
val itemsRemoved = _items.size;
|
val itemsRemoved = _items.size;
|
||||||
@@ -157,6 +162,15 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||||
|
if (!_limitToastShown) {
|
||||||
|
_limitToastShown = true;
|
||||||
|
UIDialogs.toast(context, "Stopped after $MAXIMUM_BATCH_SIZE to avoid rate limit, re-enter to import rest");
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_taskLoadChannel.run(_links[_currentLoadIndex]);
|
_taskLoadChannel.run(_links[_currentLoadIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +210,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "ImportSubscriptionsFragment";
|
val TAG = "ImportSubscriptionsFragment";
|
||||||
|
private const val MAXIMUM_BATCH_SIZE = 75;
|
||||||
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-10
@@ -346,24 +346,24 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_rating.visibility = VISIBLE;
|
_rating.visibility = VISIBLE;
|
||||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||||
_rating.onLikeDislikeUpdated.subscribe(this) { processHandle, newHasLiked, newHasDisliked ->
|
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||||
if (newHasLiked) {
|
if (args.hasLiked) {
|
||||||
processHandle.opinion(ref, Opinion.like);
|
args.processHandle.opinion(ref, Opinion.like);
|
||||||
} else if (newHasDisliked) {
|
} else if (args.hasDisliked) {
|
||||||
processHandle.opinion(ref, Opinion.dislike);
|
args.processHandle.opinion(ref, Opinion.dislike);
|
||||||
} else {
|
} else {
|
||||||
processHandle.opinion(ref, Opinion.neutral);
|
args.processHandle.opinion(ref, Opinion.neutral);
|
||||||
}
|
}
|
||||||
|
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServers();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.updateLikeMap(ref, newHasLiked, newHasDisliked)
|
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -601,7 +601,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
val subscribers = value?.author?.subscribers;
|
val subscribers = value?.author?.subscribers;
|
||||||
if(subscribers != null && subscribers > 0) {
|
if(subscribers != null && subscribers > 0) {
|
||||||
_channelMeta.visibility = View.VISIBLE;
|
_channelMeta.visibility = View.VISIBLE;
|
||||||
_channelMeta.text = value.author.subscribers!!.toHumanNumber() + " subscribers";
|
_channelMeta.text = if((value.author?.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.visibility = View.GONE;
|
_channelMeta.visibility = View.GONE;
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
|
|||||||
+15
-1
@@ -258,11 +258,25 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, "Management",
|
BigButtonGroup(c, "Management",
|
||||||
BigButton(c, "Uninstall", "Removes the plugin from the app", R.drawable.ic_block) {
|
BigButton(c, "Uninstall", "Removes the plugin from the app", R.drawable.ic_block) {
|
||||||
uninstallSource();
|
uninstallSource();
|
||||||
}.withBackground(R.drawable.background_big_button_red)
|
}.withBackground(R.drawable.background_big_button_red).apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
if(clientIfExists?.captchaEncrypted != null)
|
||||||
|
BigButton(c, "Delete Captcha", "Deletes stored captcha answer for this plugin", R.drawable.ic_block) {
|
||||||
|
clientIfExists?.updateCaptcha(null);
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
}.withBackground(R.drawable.background_big_button_red)
|
||||||
|
else null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+59
-3
@@ -12,13 +12,17 @@ import com.futo.platformplayer.*
|
|||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
|
import com.futo.platformplayer.exceptions.RateLimitException
|
||||||
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.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
@@ -28,8 +32,10 @@ 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.subscriptions.SubscriptionBar
|
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -129,6 +135,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!StateSubscriptions.instance.isGlobalUpdating) {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
@@ -155,8 +165,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
private val _filterLock = Object();
|
private val _filterLock = Object();
|
||||||
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
||||||
|
|
||||||
|
private var _bypassRateLimit = false;
|
||||||
private val _lastExceptions: List<Throwable>? = null;
|
private val _lastExceptions: List<Throwable>? = null;
|
||||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||||
|
if(!_bypassRateLimit) {
|
||||||
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||||
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
||||||
|
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||||
|
Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr);
|
||||||
|
if(rateLimitPlugins.any())
|
||||||
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
|
}
|
||||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
||||||
|
|
||||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
||||||
@@ -166,9 +185,37 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
|
.exception<RateLimitException> {
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
val subsByLimited = it.pluginIds.map{ StatePlatform.instance.getClientOrNull(it) }
|
||||||
|
.filterIsInstance<JSClient>()
|
||||||
|
.associateWith { client -> subs.filter { it.getClient() == client } }
|
||||||
|
.map { Pair(it.key, it.value) }
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_security_pred,
|
||||||
|
"Rate Limit Warning", "This is a temporary measure to prevent people from hitting rate limit until we have better support for lots of subscriptions." +
|
||||||
|
"\n\nYou have too many subscriptions for the following plugins:\n",
|
||||||
|
subsByLimited.map { "${it.first.config.name}: ${it.second.size} Subscriptions" } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
||||||
|
_bypassRateLimit = true;
|
||||||
|
loadResults();
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
||||||
|
UIDialogs.Action("OK", {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
setLoading(false);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
if(it !is CancellationException)
|
||||||
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||||
|
else {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private fun initializeToolbarContent() {
|
private fun initializeToolbarContent() {
|
||||||
@@ -234,8 +281,12 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
Logger.i(TAG, "Subscriptions load");
|
Logger.i(TAG, "Subscriptions load");
|
||||||
if(recyclerData.results.size == 0) {
|
if(recyclerData.results.size == 0) {
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||||
Logger.i(TAG, "Subscription show cache (${cachePager.getResults().size})");
|
val results = cachePager.getResults();
|
||||||
|
Logger.i(TAG, "Subscription show cache (${results.size})");
|
||||||
|
setTextCentered(if (results.isEmpty()) "No results found\nSwipe down to refresh" else null);
|
||||||
setPager(cachePager);
|
setPager(cachePager);
|
||||||
|
} else {
|
||||||
|
setTextCentered(null);
|
||||||
}
|
}
|
||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
@@ -248,10 +299,15 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
|
setTextCentered(if (pager.getResults().isEmpty()) "No results found\nSwipe down to refresh" else null);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to finish loading", e)
|
Logger.e(TAG, "Failed to finish loading", e)
|
||||||
}
|
}
|
||||||
}
|
}/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway
|
||||||
|
if(it is CancellationException) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleExceptions(exs: List<Throwable>) {
|
private fun handleExceptions(exs: List<Throwable>) {
|
||||||
|
|||||||
+7
-7
@@ -306,12 +306,12 @@ class VideoDetailFragment : MainFragment {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.i(TAG, "onResume");
|
Logger.v(TAG, "onResume");
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
_leavingPiP = false;
|
_leavingPiP = false;
|
||||||
|
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
Logger.i(TAG, "onResume preventPictureInPicture=false");
|
Logger.v(TAG, "onResume preventPictureInPicture=false");
|
||||||
it.preventPictureInPicture = false;
|
it.preventPictureInPicture = false;
|
||||||
|
|
||||||
if (state != State.CLOSED) {
|
if (state != State.CLOSED) {
|
||||||
@@ -326,7 +326,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.i(TAG, "onPause");
|
Logger.v(TAG, "onPause");
|
||||||
_isActive = false;
|
_isActive = false;
|
||||||
|
|
||||||
if(!isInPictureInPicture && state != State.CLOSED)
|
if(!isInPictureInPicture && state != State.CLOSED)
|
||||||
@@ -334,7 +334,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
Logger.i(TAG, "onStop");
|
Logger.v(TAG, "onStop");
|
||||||
|
|
||||||
stopIfRequired();
|
stopIfRequired();
|
||||||
super.onStop();
|
super.onStop();
|
||||||
@@ -352,7 +352,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
shouldStop = false;
|
shouldStop = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "shouldStop: $shouldStop");
|
Logger.v(TAG, "shouldStop: $shouldStop");
|
||||||
if(shouldStop) {
|
if(shouldStop) {
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
val v = it.video ?: return@let;
|
val v = it.video ?: return@let;
|
||||||
@@ -361,13 +361,13 @@ class VideoDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_viewDetail?.onStop();
|
_viewDetail?.onStop();
|
||||||
StateCasting.instance.onStop();
|
StateCasting.instance.onStop();
|
||||||
Logger.i(TAG, "called onStop() shouldStop: $shouldStop");
|
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
Logger.i(TAG, "onDestroyMainView");
|
Logger.v(TAG, "onDestroyMainView");
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
_viewDetail = null;
|
_viewDetail = null;
|
||||||
it.onDestroy();
|
it.onDestroy();
|
||||||
|
|||||||
+91
-25
@@ -100,6 +100,7 @@ import kotlinx.coroutines.*
|
|||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
@@ -232,9 +233,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
private val DP_5 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
private val DP_5 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
private val DP_2 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
|
private val DP_2 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
|
||||||
|
|
||||||
private var _retryJob: Job? = null;
|
private var _retryJob: Job? = null;
|
||||||
private var _retryCount = 0;
|
private var _retryCount = 0;
|
||||||
private val _retryIntervals: Array<Long> = arrayOf(1, 2, 4, 8, 16, 32);
|
//TODO: Determine better behavior, waiting 60 seconds for an error that is guaranteed to happen is a bit much. (Needed? If so, maybe need special UI for retrying)
|
||||||
|
private val _retryIntervals: Array<Long> = arrayOf(1, 1);//2, 4, 8, 16, 32);
|
||||||
|
|
||||||
|
private var _liveTryJob: Job? = null;
|
||||||
|
private val _liveStreamCheckInterval = listOf(
|
||||||
|
Pair(-10 * 60, 5 * 60), //around 10 minutes, try every 5 minute
|
||||||
|
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
||||||
|
Pair(0, 10) //around live, try every 10 seconds
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.fragview_video_detail, this);
|
inflate(context, R.layout.fragview_video_detail, this);
|
||||||
@@ -491,7 +502,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
|
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
|
||||||
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
|
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
|
||||||
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
|
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
|
||||||
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo() };
|
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true) };
|
||||||
MediaControlReceiver.onPreviousReceived.subscribe(this) { prevVideo() };
|
MediaControlReceiver.onPreviousReceived.subscribe(this) { prevVideo() };
|
||||||
MediaControlReceiver.onCloseReceived.subscribe(this) {
|
MediaControlReceiver.onCloseReceived.subscribe(this) {
|
||||||
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
|
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
|
||||||
@@ -597,7 +608,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_download, "Download", TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, "Download", TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(context.contentResolver, it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_share, "Share", TAG_SHARE) {
|
RoundButton(context, R.drawable.ic_share, "Share", TAG_SHARE) {
|
||||||
@@ -660,7 +671,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
Logger.i(TAG, "onResume");
|
Logger.v(TAG, "onResume");
|
||||||
_onPauseCalled = false;
|
_onPauseCalled = false;
|
||||||
|
|
||||||
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
|
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
|
||||||
@@ -694,7 +705,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.updateRotateLock();
|
_player.updateRotateLock();
|
||||||
}
|
}
|
||||||
fun onPause() {
|
fun onPause() {
|
||||||
Logger.i(TAG, "onPause");
|
Logger.v(TAG, "onPause");
|
||||||
|
|
||||||
_onPauseCalled = true;
|
_onPauseCalled = true;
|
||||||
_taskLoadVideo.cancel();
|
_taskLoadVideo.cancel();
|
||||||
@@ -722,6 +733,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_overlay_quality_selector?.hide();
|
_overlay_quality_selector?.hide();
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
_taskLoadVideo.cancel();
|
_taskLoadVideo.cancel();
|
||||||
handleStop();
|
handleStop();
|
||||||
_didStop = true;
|
_didStop = true;
|
||||||
@@ -808,6 +821,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
_retryCount = 0;
|
_retryCount = 0;
|
||||||
fetchVideo();
|
fetchVideo();
|
||||||
|
|
||||||
@@ -858,7 +873,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
_playWhenReady = true;
|
_playWhenReady = true;
|
||||||
if(video.author.subscribers != null) {
|
if(video.author.subscribers != null) {
|
||||||
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
|
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
@@ -897,6 +912,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
_retryCount = 0;
|
_retryCount = 0;
|
||||||
fetchVideo();
|
fetchVideo();
|
||||||
}
|
}
|
||||||
@@ -965,7 +982,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_title.text = video.name;
|
_title.text = video.name;
|
||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
if(video.author.subscribers != null) {
|
if(video.author.subscribers != null) {
|
||||||
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
|
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
@@ -1025,24 +1042,24 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||||
_rating.onLikeDislikeUpdated.subscribe(this) { processHandle, newHasLiked, newHasDisliked ->
|
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||||
if (newHasLiked) {
|
if (args.hasLiked) {
|
||||||
processHandle.opinion(ref, Opinion.like);
|
args.processHandle.opinion(ref, Opinion.like);
|
||||||
} else if (newHasDisliked) {
|
} else if (args.hasDisliked) {
|
||||||
processHandle.opinion(ref, Opinion.dislike);
|
args.processHandle.opinion(ref, Opinion.dislike);
|
||||||
} else {
|
} else {
|
||||||
processHandle.opinion(ref, Opinion.neutral);
|
args.processHandle.opinion(ref, Opinion.neutral);
|
||||||
}
|
}
|
||||||
|
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServers();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.updateLikeMap(ref, newHasLiked, newHasDisliked)
|
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -1130,6 +1147,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(video.isLive && video.live != null) {
|
if(video.isLive && video.live != null) {
|
||||||
loadLiveChat(video);
|
loadLiveChat(video);
|
||||||
}
|
}
|
||||||
|
if(video.isLive && video.live == null && !video.video.videoSources.any())
|
||||||
|
startLiveTry(video);
|
||||||
|
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
}
|
}
|
||||||
@@ -1259,7 +1278,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
//If LiveStream, set to end
|
//If LiveStream, set to end
|
||||||
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
||||||
if (video?.isLive == true) {
|
if (video?.isLive == true) {
|
||||||
_player.seekToEnd(5000);
|
_player.seekToEnd(6000);
|
||||||
}
|
}
|
||||||
|
|
||||||
val videoTracks = _player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
val videoTracks = _player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
||||||
@@ -1344,9 +1363,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextVideo(): Boolean {
|
fun nextVideo(forceLoop: Boolean = false): Boolean {
|
||||||
Logger.i(TAG, "nextVideo")
|
Logger.i(TAG, "nextVideo")
|
||||||
val next = StatePlayer.instance.nextQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
var next = StatePlayer.instance.nextQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||||
|
if(next == null && forceLoop)
|
||||||
|
next = StatePlayer.instance.restartQueue();
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next);
|
setVideoOverview(next);
|
||||||
return true;
|
return true;
|
||||||
@@ -1589,10 +1610,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_lastSubtitleSource = toSet;
|
_lastSubtitleSource = toSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnavailableVideo() {
|
private fun handleUnavailableVideo(msg: String? = null) {
|
||||||
if (!nextVideo()) {
|
if (!nextVideo()) {
|
||||||
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
|
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", "This video is unavailable.", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", msg ?: "This video is unavailable.", null, 0,
|
||||||
UIDialogs.Action("Back", {
|
UIDialogs.Action("Back", {
|
||||||
this@VideoDetailView.onClose.emit();
|
this@VideoDetailView.onClose.emit();
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
@@ -1961,7 +1982,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
Logger.i(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})");
|
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})");
|
||||||
isOverlayed?.let{ _cast.setProgressBarOverlayed(it) };
|
isOverlayed?.let{ _cast.setProgressBarOverlayed(it) };
|
||||||
|
|
||||||
if(isOverlayed == null) {
|
if(isOverlayed == null) {
|
||||||
@@ -2071,7 +2092,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
.exception<ScriptUnavailableException> {
|
.exception<ScriptUnavailableException> {
|
||||||
Logger.w(TAG, "exception<ScriptUnavailableException>", it);
|
Logger.w(TAG, "exception<ScriptUnavailableException>", it);
|
||||||
handleUnavailableVideo();
|
handleUnavailableVideo(it.message);
|
||||||
}
|
}
|
||||||
.exception<ScriptException> {
|
.exception<ScriptException> {
|
||||||
Logger.w(TAG, "exception<ScriptException>", it)
|
Logger.w(TAG, "exception<ScriptException>", it)
|
||||||
@@ -2080,6 +2101,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryCount = 0;
|
_retryCount = 0;
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video (ScriptException)", it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video (ScriptException)", it, ::fetchVideo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2090,6 +2113,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryCount = 0;
|
_retryCount = 0;
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video", it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video", it, ::fetchVideo);
|
||||||
}
|
}
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
@@ -2107,14 +2132,16 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Log.i(TAG, "handleErrorOrCall _retryCount=$_retryCount, starting retry job");
|
Log.i(TAG, "handleErrorOrCall _retryCount=$_retryCount, starting retry job");
|
||||||
|
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
_retryJob = StateApp.instance.scopeGetter().launch(Dispatchers.Main) {
|
_retryJob = StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
delay(_retryIntervals[_retryCount++] * 1000);
|
delay(_retryIntervals[_retryCount++] * 1000);
|
||||||
fetchVideo();
|
fetchVideo();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to fetch video.", e)
|
Logger.e(TAG, "Failed to retry fetch video.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = null;
|
||||||
} else if (isConnected && nextVideo()) {
|
} else if (isConnected && nextVideo()) {
|
||||||
Log.i(TAG, "handleErrorOrCall retries failed, is connected, skipped to next video");
|
Log.i(TAG, "handleErrorOrCall retries failed, is connected, skipped to next video");
|
||||||
} else {
|
} else {
|
||||||
@@ -2123,6 +2150,45 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startLiveTry(liveTryVideo: IPlatformVideoDetails) {
|
||||||
|
val datetime = liveTryVideo.datetime ?: return;
|
||||||
|
val diffSeconds = datetime.getNowDiffSeconds();
|
||||||
|
val toWait = _liveStreamCheckInterval.toList().sortedBy { abs(diffSeconds - it.first) }.firstOrNull()?.second?.toLong() ?: return;
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main){
|
||||||
|
UIDialogs.toast(context, "Not yet available, retrying in ${toWait}s");
|
||||||
|
}
|
||||||
|
|
||||||
|
_liveTryJob?.cancel();
|
||||||
|
_liveTryJob = fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
delay(toWait * 1000);
|
||||||
|
val videoDetail = StatePlatform.instance.getContentDetails(liveTryVideo.url, true).await();
|
||||||
|
if(videoDetail !is IPlatformVideoDetails)
|
||||||
|
throw IllegalStateException("Expected media content, found ${video?.contentType}");
|
||||||
|
|
||||||
|
if(videoDetail.datetime != null && videoDetail.live == null && !videoDetail.video.videoSources.any()) {
|
||||||
|
if(videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Planned in ${videoDetail.datetime?.toHumanNowDiffString(true)}");
|
||||||
|
}
|
||||||
|
startLiveTry(liveTryVideo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setVideoDetails(videoDetail);
|
||||||
|
_liveTryJob = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to live try fetch video.", ex);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Failed to retry for live stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun applyFragment(frag: VideoDetailFragment) {
|
fun applyFragment(frag: VideoDetailFragment) {
|
||||||
fragment = frag;
|
fragment = frag;
|
||||||
fragment.onMinimize.subscribe {
|
fragment.onMinimize.subscribe {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import android.net.Uri
|
|||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
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.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.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.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.JSVideoUrlRangeSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -17,6 +20,12 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
|
|||||||
class VideoHelper {
|
class VideoHelper {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
fun isDownloadable(detail: IPlatformVideoDetails) =
|
||||||
|
(detail.video.videoSources.any { isDownloadable(it) }) ||
|
||||||
|
(if (detail is VideoUnMuxedSourceDescriptor) detail.audioSources.any { isDownloadable(it) } else false);
|
||||||
|
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource;
|
||||||
|
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource;
|
||||||
|
|
||||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||||
val targetVideo = if(desiredPixelCount > 0)
|
val targetVideo = if(desiredPixelCount > 0)
|
||||||
|
|||||||
@@ -61,9 +61,21 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
|||||||
_deferred.invokeOnCompletion(throwable -> {
|
_deferred.invokeOnCompletion(throwable -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
callback.onLoadFailed(new Exception(throwable));
|
callback.onLoadFailed(new Exception(throwable));
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Deferred<ByteBuffer> deferred = _deferred;
|
||||||
|
if (deferred == null) {
|
||||||
|
callback.onLoadFailed(new Exception("Deferred is null"));
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer completed = deferred.getCompleted();
|
||||||
|
if (completed != null) {
|
||||||
|
callback.onDataReady(completed);
|
||||||
|
} else {
|
||||||
|
callback.onLoadFailed(new Exception("Completed is null"));
|
||||||
}
|
}
|
||||||
final ByteBuffer completed = _deferred.getCompleted();
|
|
||||||
callback.onDataReady(completed);
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,48 @@ package com.futo.platformplayer.models
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.getNowDiffDays
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class Subscription {
|
class Subscription {
|
||||||
var channel: SerializedChannel;
|
var channel: SerializedChannel;
|
||||||
|
|
||||||
|
//Last found content
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
|
var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastLiveStream : OffsetDateTime = OffsetDateTime.MAX;
|
var lastLiveStream : OffsetDateTime = OffsetDateTime.MAX;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastPost : OffsetDateTime = OffsetDateTime.MAX;
|
||||||
|
|
||||||
|
//Last update time
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastVideoUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastLiveStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
|
||||||
|
//Last video interval
|
||||||
var uploadInterval : Int = 0;
|
var uploadInterval : Int = 0;
|
||||||
|
var uploadPostInterval : Int = 0;
|
||||||
|
|
||||||
|
|
||||||
constructor(channel : SerializedChannel) {
|
constructor(channel : SerializedChannel) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldFetchStreams() = lastLiveStream.getNowDiffDays() < 7;
|
||||||
|
fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14;
|
||||||
|
fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2;
|
||||||
|
|
||||||
|
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
|
|
||||||
fun updateChannel(channel: IPlatformChannel) {
|
fun updateChannel(channel: IPlatformChannel) {
|
||||||
this.channel = SerializedChannel.fromChannel(channel);
|
this.channel = SerializedChannel.fromChannel(channel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ data class Telemetry(
|
|||||||
val buildType: String,
|
val buildType: String,
|
||||||
val debug: Boolean,
|
val debug: Boolean,
|
||||||
val isUnstableBuild: Boolean,
|
val isUnstableBuild: Boolean,
|
||||||
val time: Long,
|
|
||||||
val brand: String,
|
val brand: String,
|
||||||
val manufacturer: String,
|
val manufacturer: String,
|
||||||
val model: String
|
val model: String
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.futo.platformplayer.others
|
||||||
|
|
||||||
|
import android.webkit.*
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginCaptchaConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
|
class CaptchaWebViewClient : WebViewClient {
|
||||||
|
val onCaptchaFinished = Event1<SourceCaptchaData?>();
|
||||||
|
val onPageLoaded = Event2<WebView?, String?>()
|
||||||
|
|
||||||
|
private val _pluginConfig: SourcePluginConfig?;
|
||||||
|
private val _captchaConfig: SourcePluginCaptchaConfig;
|
||||||
|
|
||||||
|
private var _didNotify = false;
|
||||||
|
private val _extractor: WebViewRequirementExtractor;
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig) : super() {
|
||||||
|
_pluginConfig = config;
|
||||||
|
_captchaConfig = config.captcha!!;
|
||||||
|
_extractor = WebViewRequirementExtractor(
|
||||||
|
config.allowUrls,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
config.captcha!!.cookiesToFind,
|
||||||
|
config.captcha!!.completionUrl,
|
||||||
|
config.captcha!!.cookiesExclOthers
|
||||||
|
);
|
||||||
|
Logger.i(TAG, "Captcha [${config.name}]" +
|
||||||
|
"\nRequired Cookies: ${Serializer.json.encodeToString(config.captcha!!.cookiesToFind)}",);
|
||||||
|
}
|
||||||
|
constructor(captcha: SourcePluginCaptchaConfig) : super() {
|
||||||
|
_pluginConfig = null;
|
||||||
|
_captchaConfig = captcha;
|
||||||
|
_extractor = WebViewRequirementExtractor(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
captcha.cookiesToFind,
|
||||||
|
captcha.completionUrl,
|
||||||
|
captcha.cookiesExclOthers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
Logger.i(TAG, "onPageFinished url = ${url}")
|
||||||
|
onPageLoaded.emit(view, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||||
|
if(request == null)
|
||||||
|
return super.shouldInterceptRequest(view, request as WebResourceRequest?);
|
||||||
|
|
||||||
|
val extracted = _extractor.handleRequest(view, request);
|
||||||
|
if(extracted != null && !_didNotify) {
|
||||||
|
_didNotify = true;
|
||||||
|
onCaptchaFinished.emit(SourceCaptchaData(
|
||||||
|
extracted.cookies,
|
||||||
|
extracted.headers
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CaptchaWebViewClient";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.others
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.*
|
import android.webkit.*
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -12,6 +13,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class LoginWebViewClient : WebViewClient {
|
class LoginWebViewClient : WebViewClient {
|
||||||
private val LOG_VERBOSE = false;
|
private val LOG_VERBOSE = false;
|
||||||
@@ -42,10 +44,13 @@ class LoginWebViewClient : WebViewClient {
|
|||||||
private var urlFound = false;
|
private var urlFound = false;
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
if(BuildConfig.DEBUG)
|
||||||
|
Logger.i(TAG, "Login Url Page: " + url);
|
||||||
super.onPageFinished(view, url);
|
super.onPageFinished(view, url);
|
||||||
onPageLoaded.emit(view, url);
|
onPageLoaded.emit(view, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Use new WebViewRequirementExtractor when time to test extensively
|
||||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||||
if(request == null)
|
if(request == null)
|
||||||
return super.shouldInterceptRequest(view, request as WebResourceRequest?);
|
return super.shouldInterceptRequest(view, request as WebResourceRequest?);
|
||||||
@@ -54,11 +59,29 @@ class LoginWebViewClient : WebViewClient {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var completionUrlExcludeQuery = false
|
||||||
|
var completionUrlToCheck = if(urlFound) null else _authConfig.completionUrl;
|
||||||
|
if(completionUrlToCheck != null) {
|
||||||
|
if(completionUrlToCheck.endsWith("?*")) {
|
||||||
|
completionUrlToCheck = completionUrlToCheck.substring(0, completionUrlToCheck.length - 2);
|
||||||
|
completionUrlExcludeQuery = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val domain = request.url.host;
|
val domain = request.url.host;
|
||||||
val domainLower = request.url.host?.lowercase();
|
val domainLower = request.url.host?.lowercase();
|
||||||
|
val urlString = request.url.toString();
|
||||||
if(_authConfig.completionUrl == null)
|
if(_authConfig.completionUrl == null)
|
||||||
urlFound = true;
|
urlFound = true;
|
||||||
else urlFound = urlFound || request.url == Uri.parse(_authConfig.completionUrl);
|
else urlFound = urlFound || (
|
||||||
|
if(completionUrlExcludeQuery)
|
||||||
|
(if(urlString.contains("?"))
|
||||||
|
urlString.substring(0, urlString.indexOf("?")) == completionUrlToCheck
|
||||||
|
else urlString == completionUrlToCheck)
|
||||||
|
else
|
||||||
|
request.url == Uri.parse(_authConfig.completionUrl)
|
||||||
|
);
|
||||||
|
|
||||||
//HEADERS
|
//HEADERS
|
||||||
if(domainLower != null) {
|
if(domainLower != null) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.futo.platformplayer.others
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
|
||||||
|
class WebViewRequirementExtractor {
|
||||||
|
private val allowedUrls: List<String>;
|
||||||
|
private val headersToFind: List<String>?;
|
||||||
|
private val domainHeadersToFind: Map<String, List<String>>?;
|
||||||
|
private val cookiesToFind: List<String>?;
|
||||||
|
private val completionUrl: String?;
|
||||||
|
|
||||||
|
private val exclOtherCookies: Boolean;
|
||||||
|
|
||||||
|
|
||||||
|
private val headersFoundMap: HashMap<String, HashMap<String, String>> = hashMapOf();
|
||||||
|
private val cookiesFoundMap = hashMapOf<String, HashMap<String, String>>();
|
||||||
|
private var urlFound = false;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(allowedUrls: List<String>?, headers: List<String>?, domainHeaders: Map<String, List<String>>?, cookies: List<String>?, url: String?, exclOtherCookies: Boolean = false) {
|
||||||
|
this.allowedUrls = allowedUrls ?: listOf("everywhere");
|
||||||
|
this.exclOtherCookies = exclOtherCookies;
|
||||||
|
headersToFind = headers;
|
||||||
|
domainHeadersToFind = domainHeaders;
|
||||||
|
cookiesToFind = cookies;
|
||||||
|
completionUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun handleRequest(view: WebView?, request: WebResourceRequest, logVerbose: Boolean = false): ExtractedData? {
|
||||||
|
|
||||||
|
val domain = request.url.host;
|
||||||
|
val domainLower = request.url.host?.lowercase();
|
||||||
|
if(completionUrl == null)
|
||||||
|
urlFound = true;
|
||||||
|
else urlFound = urlFound || request.url == Uri.parse(completionUrl);
|
||||||
|
|
||||||
|
//HEADERS
|
||||||
|
if(domainLower != null) {
|
||||||
|
val headersToFind = ((headersToFind?.map { Pair(it.lowercase(), domainLower) } ?: listOf()) +
|
||||||
|
(domainHeadersToFind?.filter { domainLower.matchesDomain(it.key.lowercase())}
|
||||||
|
?.flatMap { it.value.map { header -> Pair(header.lowercase(), it.key.lowercase()) } } ?: listOf()));
|
||||||
|
|
||||||
|
val foundHeaders = request.requestHeaders.filter { requestHeader -> headersToFind.any { it.first.equals(requestHeader.key, true)} &&
|
||||||
|
(!requestHeader.key.equals("Authorization", ignoreCase = true) || requestHeader.value != "undefined") } //TODO: More universal fix (optional regex?)
|
||||||
|
for(header in foundHeaders) {
|
||||||
|
for(headerDomain in headersToFind.filter { it.first.equals(header.key, true) }) {
|
||||||
|
if (!headersFoundMap.containsKey(headerDomain.second))
|
||||||
|
headersFoundMap[headerDomain.second] = hashMapOf();
|
||||||
|
headersFoundMap[headerDomain.second]!![header.key.lowercase()] = header.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//COOKIES
|
||||||
|
//TODO: This is not an ideal solution, we want to intercept the response, but interception need to be rewritten to support that. Correct implementation commented underneath
|
||||||
|
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
|
||||||
|
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
|
||||||
|
if(cookieString != null) {
|
||||||
|
val domainParts = domain!!.split(".");
|
||||||
|
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
|
if(allowedUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
|
||||||
|
cookiesToFind?.let { cookiesToFind ->
|
||||||
|
val cookies = cookieString.split(";");
|
||||||
|
for(cookieStr in cookies) {
|
||||||
|
val cookieSplitIndex = cookieStr.indexOf("=");
|
||||||
|
if(cookieSplitIndex <= 0) continue;
|
||||||
|
val cookieKey = cookieStr.substring(0, cookieSplitIndex).trim();
|
||||||
|
val cookieVal = cookieStr.substring(cookieSplitIndex + 1).trim();
|
||||||
|
|
||||||
|
if (exclOtherCookies && !cookiesToFind.contains(cookieKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cookiesFoundMap.containsKey(cookieDomain))
|
||||||
|
cookiesFoundMap[cookieDomain]!![cookieKey] = cookieVal;
|
||||||
|
else
|
||||||
|
cookiesFoundMap[cookieDomain] = hashMapOf(Pair(cookieKey, cookieVal));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
val headersFound = headersToFind?.map { it.lowercase() }?.all { reqHeader -> headersFoundMap.any { it.value.containsKey(reqHeader) } } ?: true
|
||||||
|
val domainHeadersFound = domainHeadersToFind?.all {
|
||||||
|
if(it.value.isEmpty())
|
||||||
|
return@all true;
|
||||||
|
if(!headersFoundMap.containsKey(it.key.lowercase()))
|
||||||
|
return@all false;
|
||||||
|
val foundDomainHeaders = headersFoundMap[it.key.lowercase()] ?: mapOf();
|
||||||
|
return@all it.value.all { reqHeader -> foundDomainHeaders.containsKey(reqHeader.lowercase()) };
|
||||||
|
} ?: true;
|
||||||
|
val cookiesFound = cookiesToFind?.all { toFind -> cookiesFoundMap.any { it.value.containsKey(toFind) } } ?: true;
|
||||||
|
|
||||||
|
if(logVerbose) {
|
||||||
|
val builder = StringBuilder();
|
||||||
|
builder.appendLine("Request (method: ${request.method}, host: ${request.url.host}, url: ${request.url}, path: ${request.url.path}):");
|
||||||
|
for (pair in request.requestHeaders) {
|
||||||
|
builder.appendLine(" ${pair.key}: ${pair.value}");
|
||||||
|
}
|
||||||
|
builder.appendLine(" Cookies: ${cookiesFoundMap.values.sumOf { it.values.size }}");
|
||||||
|
Logger.i(TAG, builder.toString());
|
||||||
|
Logger.i(TAG, "Result (urlFound: $urlFound, headersFound: $headersFound, cookiesFound: $cookiesFound)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlFound && headersFound && domainHeadersFound && cookiesFound)
|
||||||
|
return ExtractedData(cookiesFoundMap, headersFoundMap);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data class ExtractedData(
|
||||||
|
val cookies: HashMap<String, HashMap<String, String>>,
|
||||||
|
val headers: HashMap<String, HashMap<String, String>>
|
||||||
|
);
|
||||||
|
companion object {
|
||||||
|
val TAG = "WebViewRequirementExtractor";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,10 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
|||||||
if(obj?.jsonPrimitive?.isString ?: true)
|
if(obj?.jsonPrimitive?.isString ?: true)
|
||||||
return when(obj?.jsonPrimitive?.contentOrNull) {
|
return when(obj?.jsonPrimitive?.contentOrNull) {
|
||||||
"MEDIA" -> SerializedPlatformVideo.serializer();
|
"MEDIA" -> SerializedPlatformVideo.serializer();
|
||||||
"NESTED" -> SerializedPlatformNestedContent.serializer();
|
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
||||||
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
||||||
"POST" -> throw NotImplementedError("Post not yet implemented");
|
"POST" -> throw NotImplementedError("Post not yet implemented");
|
||||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}")
|
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
|
||||||
};
|
};
|
||||||
else
|
else
|
||||||
return when(obj?.jsonPrimitive?.int) {
|
return when(obj?.jsonPrimitive?.int) {
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import com.futo.platformplayer.*
|
|||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.Announcement
|
import com.futo.platformplayer.states.Announcement
|
||||||
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.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -134,15 +136,21 @@ class DownloadService : Service() {
|
|||||||
Logger.w(TAG, "Video had no video or videodetail, removing download");
|
Logger.w(TAG, "Video had no video or videodetail, removing download");
|
||||||
StateDownloads.instance.removeDownload(currentVideo);
|
StateDownloads.instance.removeDownload(currentVideo);
|
||||||
}
|
}
|
||||||
|
else if(ex is DownloadException && !ex.isRetryable) {
|
||||||
|
Logger.w(TAG, "Video had exception that should not be retried");
|
||||||
|
StateDownloads.instance.removeDownload(currentVideo);
|
||||||
|
StateDownloads.instance.preventPlaylistDownload(currentVideo);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex);
|
Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex);
|
||||||
currentVideo.error = ex.message;
|
currentVideo.error = ex.message;
|
||||||
currentVideo.changeState(VideoDownload.State.ERROR);
|
currentVideo.changeState(VideoDownload.State.ERROR);
|
||||||
ignore.add(currentVideo);
|
ignore.add(currentVideo);
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload",
|
if(ex !is CancellationException)
|
||||||
"Download failed",
|
StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload",
|
||||||
"Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download");
|
"Download failed",
|
||||||
|
"Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download");
|
||||||
|
|
||||||
//Give it a sec
|
//Give it a sec
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class MediaPlaybackService : Service() {
|
|||||||
private var _focusRequest: AudioFocusRequest? = null;
|
private var _focusRequest: AudioFocusRequest? = null;
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Logger.i(TAG, "onStartCommand");
|
Logger.v(TAG, "onStartCommand");
|
||||||
|
|
||||||
|
|
||||||
if(!FragmentedStorage.isInitialized) {
|
if(!FragmentedStorage.isInitialized) {
|
||||||
@@ -91,43 +91,49 @@ class MediaPlaybackService : Service() {
|
|||||||
_mediaSession?.setCallback(object: MediaSessionCompat.Callback() {
|
_mediaSession?.setCallback(object: MediaSessionCompat.Callback() {
|
||||||
override fun onSeekTo(pos: Long) {
|
override fun onSeekTo(pos: Long) {
|
||||||
super.onSeekTo(pos)
|
super.onSeekTo(pos)
|
||||||
Log.i(TAG, "Media session callback onSeekTo(pos = $pos)");
|
Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)");
|
||||||
MediaControlReceiver.onSeekToReceived.emit(pos);
|
MediaControlReceiver.onSeekToReceived.emit(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
super.onPlay();
|
super.onPlay();
|
||||||
Log.i(TAG, "Media session callback onPlay()");
|
Logger.i(TAG, "Media session callback onPlay()");
|
||||||
MediaControlReceiver.onPlayReceived.emit();
|
MediaControlReceiver.onPlayReceived.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Log.i(TAG, "Media session callback onPause()");
|
Logger.i(TAG, "Media session callback onPause()");
|
||||||
MediaControlReceiver.onPauseReceived.emit();
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
Log.i(TAG, "Media session callback onStop()");
|
Logger.i(TAG, "Media session callback onStop()");
|
||||||
MediaControlReceiver.onCloseReceived.emit();
|
MediaControlReceiver.onCloseReceived.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
super.onSkipToPrevious();
|
super.onSkipToPrevious();
|
||||||
Log.i(TAG, "Media session callback onSkipToPrevious()");
|
Logger.i(TAG, "Media session callback onSkipToPrevious()");
|
||||||
MediaControlReceiver.onPreviousReceived.emit();
|
MediaControlReceiver.onPreviousReceived.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSkipToNext() {
|
||||||
|
super.onSkipToNext()
|
||||||
|
Logger.i(TAG, "Media session callback onSkipToNext()");
|
||||||
|
MediaControlReceiver.onNextReceived.emit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
Logger.i(TAG, "onCreate called");
|
Logger.v(TAG, "onCreate");
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Logger.i(TAG, "onDestroy called");
|
Logger.v(TAG, "onDestroy");
|
||||||
_instance = null;
|
_instance = null;
|
||||||
MediaControlReceiver.onCloseReceived.emit();
|
MediaControlReceiver.onCloseReceived.emit();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -138,7 +144,7 @@ class MediaPlaybackService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun closeMediaSession() {
|
fun closeMediaSession() {
|
||||||
Logger.i(TAG, "closeMediaSession called");
|
Logger.v(TAG, "closeMediaSession");
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
|
|
||||||
val focusRequest = _focusRequest;
|
val focusRequest = _focusRequest;
|
||||||
@@ -159,7 +165,7 @@ class MediaPlaybackService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateMediaSession(videoUpdated: IPlatformVideo?) {
|
fun updateMediaSession(videoUpdated: IPlatformVideo?) {
|
||||||
Logger.i(TAG, "updateMediaSession called");
|
Logger.v(TAG, "updateMediaSession");
|
||||||
var isUpdating = false;
|
var isUpdating = false;
|
||||||
val video: IPlatformVideo;
|
val video: IPlatformVideo;
|
||||||
if(videoUpdated == null) {
|
if(videoUpdated == null) {
|
||||||
@@ -270,7 +276,7 @@ class MediaPlaybackService : Service() {
|
|||||||
val notif = builder.build();
|
val notif = builder.build();
|
||||||
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
|
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
|
||||||
|
|
||||||
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "not null" else "null"} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
|
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
|
||||||
|
|
||||||
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||||
|
|
||||||
@@ -285,6 +291,7 @@ class MediaPlaybackService : Service() {
|
|||||||
PlaybackStateCompat.ACTION_PLAY or
|
PlaybackStateCompat.ACTION_PLAY or
|
||||||
PlaybackStateCompat.ACTION_PAUSE or
|
PlaybackStateCompat.ACTION_PAUSE or
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||||
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||||
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||||
)
|
)
|
||||||
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
||||||
|
|||||||
@@ -25,11 +25,19 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
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.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.logging.AndroidLogConsumer
|
import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||||
import com.futo.platformplayer.logging.FileLogConsumer
|
import com.futo.platformplayer.logging.FileLogConsumer
|
||||||
import com.futo.platformplayer.logging.LogLevel
|
import com.futo.platformplayer.logging.LogLevel
|
||||||
@@ -38,7 +46,10 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
|||||||
import com.futo.platformplayer.services.DownloadService
|
import com.futo.platformplayer.services.DownloadService
|
||||||
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 com.stripe.android.core.utils.encodeToJson
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -52,10 +63,9 @@ import java.util.concurrent.TimeUnit
|
|||||||
class StateApp {
|
class StateApp {
|
||||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||||
|
|
||||||
|
/*
|
||||||
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
|
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun getExternalRootDirectory(): File? {
|
fun getExternalRootDirectory(): File? {
|
||||||
if(!externalRootDirectory.exists()) {
|
if(!externalRootDirectory.exists()) {
|
||||||
val result = externalRootDirectory.mkdirs();
|
val result = externalRootDirectory.mkdirs();
|
||||||
@@ -65,6 +75,61 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
return externalRootDirectory;
|
return externalRootDirectory;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||||
|
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||||
|
if(isValidStorageUri(context, generalUri))
|
||||||
|
return DocumentFile.fromTreeUri(context, generalUri!!);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fun changeExternalGeneralDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) {
|
||||||
|
if(context is Context)
|
||||||
|
requestDirectoryAccess(context, "General Files", "This directory is used to save auto-backups and other persistent files.", null) {
|
||||||
|
if(it != null)
|
||||||
|
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||||
|
if(it != null && isValidStorageUri(context, it)) {
|
||||||
|
Logger.i(TAG, "Changed external general directory: ${it}");
|
||||||
|
Settings.instance.storage.storage_general = it.toString();
|
||||||
|
Settings.instance.save();
|
||||||
|
|
||||||
|
onChanged?.invoke(getExternalGeneralDirectory(context));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast("Failed to gain access to\n [${it?.lastPathSegment}]");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fun getExternalDownloadDirectory(context: Context): DocumentFile? {
|
||||||
|
val downloadUri = Settings.instance.storage.storage_download?.let { Uri.parse(it) };
|
||||||
|
if(isValidStorageUri(context, downloadUri))
|
||||||
|
return DocumentFile.fromTreeUri(context, downloadUri!!);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fun changeExternalDownloadDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) {
|
||||||
|
|
||||||
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast("External download directory not yet used by export (WIP)");
|
||||||
|
};
|
||||||
|
if(context is Context)
|
||||||
|
requestDirectoryAccess(context, "Download Exports", "This directory is used to export downloads to for external usage.", null) {
|
||||||
|
if(it != null)
|
||||||
|
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||||
|
if(it != null && isValidStorageUri(context, it)) {
|
||||||
|
Logger.i(TAG, "Changed external download directory: ${it}");
|
||||||
|
Settings.instance.storage.storage_general = it.toString();
|
||||||
|
Settings.instance.save();
|
||||||
|
|
||||||
|
onChanged?.invoke(getExternalDownloadDirectory(context));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fun isValidStorageUri(context: Context, uri: Uri?): Boolean {
|
||||||
|
if(uri == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return context.contentResolver.persistedUriPermissions.any { it.uri == uri && it.isReadPermission && it.isWritePermission };
|
||||||
}
|
}
|
||||||
|
|
||||||
//Scope
|
//Scope
|
||||||
@@ -171,20 +236,20 @@ class StateApp {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, path: Uri?, handle: (Uri?)->Unit)
|
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
||||||
{
|
{
|
||||||
if(activity is Context)
|
if(activity is Context)
|
||||||
{
|
{
|
||||||
UIDialogs.showDialog(activity, R.drawable.ic_security, "Missing Access", "Please grant access to ${name}", null, 0,
|
UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Ok", {
|
UIDialogs.Action("Ok", {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
if(path != null)
|
if(path != null)
|
||||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||||
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
.and(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.and(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
.and(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||||
|
|
||||||
activity.launchForResult(intent, 99) {
|
activity.launchForResult(intent, 99) {
|
||||||
if(it.resultCode == Activity.RESULT_OK) {
|
if(it.resultCode == Activity.RESULT_OK) {
|
||||||
@@ -299,7 +364,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.onLogSubmitted.subscribe {
|
Logger.onLogSubmitted.subscribe {
|
||||||
scopeGetter().launch(Dispatchers.Main) {
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
UIDialogs.toast("Uploaded " + (it ?: "null"), true);
|
UIDialogs.toast("Uploaded " + (it ?: "null"), true);
|
||||||
@@ -369,24 +434,50 @@ class StateApp {
|
|||||||
StatePlaylists.instance.toMigrateCheck()
|
StatePlaylists.instance.toMigrateCheck()
|
||||||
).flatten(), 0);
|
).flatten(), 0);
|
||||||
|
|
||||||
scope.launch {
|
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||||
delay(5000);
|
scope.launch(Dispatchers.IO) {
|
||||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||||
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
||||||
|
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||||
|
if (isRateLimitReached) {
|
||||||
|
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||||
|
delay(5000);
|
||||||
|
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||||
scheduleBackgroundWork(context, interval != 0, interval);
|
scheduleBackgroundWork(context, interval != 0, interval);
|
||||||
|
|
||||||
|
|
||||||
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
||||||
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
||||||
UIDialogs.showAutomaticBackupDialog(context);
|
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
StateAnnouncement.instance.deleteAnnouncement("backup");
|
UIDialogs.toast("Missing general directory");
|
||||||
|
changeExternalGeneralDirectory(context) {
|
||||||
|
UIDialogs.showAutomaticBackupDialog(context);
|
||||||
|
StateAnnouncement.instance.deleteAnnouncement("backup");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
UIDialogs.showAutomaticBackupDialog(context);
|
||||||
|
StateAnnouncement.instance.deleteAnnouncement("backup");
|
||||||
|
}
|
||||||
}, "No Backup", {
|
}, "No Backup", {
|
||||||
Settings.instance.backup.didAskAutoBackup = true;
|
Settings.instance.backup.didAskAutoBackup = true;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if(Settings.instance.backup.didAskAutoBackup && Settings.instance.backup.shouldAutomaticBackup() && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
|
if(context is IWithResultLauncher) {
|
||||||
|
Logger.i(TAG, "Backup set without general directory, please select general external directory");
|
||||||
|
changeExternalGeneralDirectory(context) {
|
||||||
|
Logger.i(TAG, "Directory set, Auto-backup should resume to this location");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -510,7 +601,6 @@ class StateApp {
|
|||||||
if (_lastNetworkState != NetworkState.DISCONNECTED) {
|
if (_lastNetworkState != NetworkState.DISCONNECTED) {
|
||||||
scopeOrNull?.launch(Dispatchers.Main) {
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "onConnectionAvailable emitted");
|
|
||||||
onConnectionAvailable.emit();
|
onConnectionAvailable.emit();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to emit onConnectionAvailable", e)
|
Logger.e(TAG, "Failed to emit onConnectionAvailable", e)
|
||||||
@@ -572,6 +662,40 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var hasCaptchaDialog = false;
|
||||||
|
fun handleCaptchaException(client: JSClient, exception: ScriptCaptchaRequiredException) {
|
||||||
|
Logger.w(HomeFragment.TAG, "[${client.name}] Plugin captcha required.", exception);
|
||||||
|
|
||||||
|
scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
if(hasCaptchaDialog)
|
||||||
|
return@launch;
|
||||||
|
hasCaptchaDialog = true;
|
||||||
|
UIDialogs.showConfirmationDialog(context, "Captcha required\nPlugin [${client.config.name}]", {
|
||||||
|
CaptchaActivity.showCaptcha(context, client.config, exception.url, exception.body) {
|
||||||
|
hasCaptchaDialog = false;
|
||||||
|
|
||||||
|
if(client is DevJSClient) {
|
||||||
|
client.setCaptcha(it);
|
||||||
|
client.recreate(context);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
|
||||||
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
StatePlatform.instance.reloadClient(context, client.config.id);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
hasCaptchaDialog = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "StateApp";
|
private val TAG = "StateApp";
|
||||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||||
|
|||||||
@@ -17,11 +17,17 @@ import com.futo.platformplayer.activities.IWithResultLauncher
|
|||||||
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.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.copyTo
|
||||||
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||||
|
import com.futo.platformplayer.getInputStream
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
|
import com.futo.platformplayer.getOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.readBytes
|
||||||
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 com.futo.platformplayer.writeBytes
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -47,20 +53,22 @@ class StateBackup {
|
|||||||
|
|
||||||
private val _autoBackupLock = Object();
|
private val _autoBackupLock = Object();
|
||||||
|
|
||||||
private fun getAutomaticBackupDocumentFiles(context: Context, root: Uri, create: Boolean = false): Pair<DocumentFile?, DocumentFile?> {
|
private fun getAutomaticBackupDocumentFiles(context: Context, create: Boolean = false): Pair<DocumentFile?, DocumentFile?> {
|
||||||
val dir = DocumentFile.fromTreeUri(context, root);
|
if(!Settings.instance.storage.isStorageMainValid(context))
|
||||||
if(dir == null)
|
return Pair(null, null);
|
||||||
throw IllegalStateException("Can't access external document files");
|
val uri = Settings.instance.storage.getStorageGeneralUri() ?: return Pair(null, null);
|
||||||
|
val dir = DocumentFile.fromTreeUri(context, uri) ?: return Pair(null, null);
|
||||||
val mainBackupFile = dir.findFile("GrayjayBackup.ezip") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip") else null;
|
val mainBackupFile = dir.findFile("GrayjayBackup.ezip") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip") else null;
|
||||||
val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
|
val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
|
||||||
return Pair(mainBackupFile, secondaryBackupFile);
|
return Pair(mainBackupFile, secondaryBackupFile);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
private fun getAutomaticBackupFiles(): Pair<File, File> {
|
private fun getAutomaticBackupFiles(): Pair<File, File> {
|
||||||
val dir = StateApp.instance.getExternalRootDirectory();
|
val dir = StateApp.instance.getExternalRootDirectory();
|
||||||
if(dir == null)
|
if(dir == null)
|
||||||
throw IllegalStateException("Can't access external files");
|
throw IllegalStateException("Can't access external files");
|
||||||
return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old"))
|
return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old"))
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
fun getAllMigrationStores(): List<ManagedStore<*>> = listOf(
|
fun getAllMigrationStores(): List<ManagedStore<*>> = listOf(
|
||||||
@@ -77,10 +85,11 @@ class StateBackup {
|
|||||||
return password.padStart(32, '9');
|
return password.padStart(32, '9');
|
||||||
}
|
}
|
||||||
fun hasAutomaticBackup(): Boolean {
|
fun hasAutomaticBackup(): Boolean {
|
||||||
if(StateApp.instance.getExternalRootDirectory() == null)
|
val context = StateApp.instance.contextOrNull ?: return false;
|
||||||
|
if(!Settings.instance.storage.isStorageMainValid(context))
|
||||||
return false;
|
return false;
|
||||||
val files = getAutomaticBackupFiles();
|
val files = getAutomaticBackupDocumentFiles(context,);
|
||||||
return files.first.exists() || files.second.exists();
|
return files.first?.exists() ?: false || files.second?.exists() ?: false;
|
||||||
}
|
}
|
||||||
fun startAutomaticBackup(force: Boolean = false) {
|
fun startAutomaticBackup(force: Boolean = false) {
|
||||||
val lastBackupHoursAgo = Settings.instance.backup.lastAutoBackupTime.getNowDiffHours();
|
val lastBackupHoursAgo = Settings.instance.backup.lastAutoBackupTime.getNowDiffHours();
|
||||||
@@ -93,20 +102,27 @@ class StateBackup {
|
|||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Starting AutoBackup (Last ${lastBackupHoursAgo} ago)");
|
Logger.i(TAG, "Starting AutoBackup (Last ${lastBackupHoursAgo} ago)");
|
||||||
synchronized(_autoBackupLock) {
|
synchronized(_autoBackupLock) {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return@synchronized;
|
||||||
val data = export();
|
val data = export();
|
||||||
val zip = data.asZip();
|
val zip = data.asZip();
|
||||||
|
|
||||||
val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
||||||
|
|
||||||
val backupFiles = getAutomaticBackupFiles();
|
if(!Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
val exportFile = backupFiles.first;
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
if (exportFile.exists())
|
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
|
||||||
exportFile.copyTo(backupFiles.second, true);
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val backupFiles = getAutomaticBackupDocumentFiles(context, true);
|
||||||
|
val exportFile = backupFiles.first;
|
||||||
|
if (exportFile?.exists() == true && backupFiles.second != null)
|
||||||
|
exportFile!!.copyTo(context, backupFiles.second!!);
|
||||||
|
exportFile!!.writeBytes(context, encryptedZip);
|
||||||
|
|
||||||
exportFile.writeBytes(encryptedZip);
|
Settings.instance.backup.lastAutoBackupTime = OffsetDateTime.now(); //OffsetDateTime.now();
|
||||||
|
Settings.instance.save();
|
||||||
Settings.instance.backup.lastAutoBackupTime = OffsetDateTime.now(); //OffsetDateTime.now();
|
}
|
||||||
Settings.instance.save();
|
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Finished AutoBackup");
|
Logger.i(TAG, "Finished AutoBackup");
|
||||||
}
|
}
|
||||||
@@ -119,28 +135,22 @@ class StateBackup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: This contains a temporary workaround to make it semi-compatible with > Android 11. By mixing "File" and "DocumentFile" usage.
|
//TODO: This goes has recently changed to use DocumentFiles and DocumentTree, and might need additional checks/edgecases covered.
|
||||||
//TODO: For now this is used to at least recover and gain temporary access to docs after losing access (due to permission lost after reinstall)
|
fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false) {
|
||||||
//TODO: Should be replaced with a more re-usable system that leverages OPEN_DOCUMENT_TREE once, and somehow persist this content after uninstall
|
|
||||||
//TODO: DocumentFiles are not compatible with normal files and require its own system.
|
|
||||||
//TODO: Investigate persistence of DOCUMENT_TREE files after uninstall...
|
|
||||||
fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false, withStream: InputStream? = null) {
|
|
||||||
if(ifExists && !hasAutomaticBackup()) {
|
if(ifExists && !hasAutomaticBackup()) {
|
||||||
Logger.i(TAG, "No AutoBackup exists, not restoring");
|
Logger.i(TAG, "No AutoBackup exists, not restoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Sadly on reinstalls of app this fails on file permissions.
|
|
||||||
|
|
||||||
Logger.i(TAG, "Starting AutoBackup restore");
|
Logger.i(TAG, "Starting AutoBackup restore");
|
||||||
synchronized(_autoBackupLock) {
|
synchronized(_autoBackupLock) {
|
||||||
|
|
||||||
val backupFiles = getAutomaticBackupFiles();
|
val backupFiles = getAutomaticBackupDocumentFiles(StateApp.instance.context, false);
|
||||||
try {
|
try {
|
||||||
if (!backupFiles.first.exists() && withStream == null)
|
if (backupFiles.first?.exists() != true)
|
||||||
throw IllegalStateException("Backup file does not exist");
|
throw IllegalStateException("Backup file does not exist");
|
||||||
|
|
||||||
val backupBytesEncrypted = if(withStream != null) withStream.readBytes() else backupFiles.first.readBytes();
|
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
||||||
importZipBytes(context, scope, backupBytes);
|
importZipBytes(context, scope, backupBytes);
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
Logger.i(TAG, "Finished AutoBackup restore");
|
||||||
@@ -154,21 +164,21 @@ class StateBackup {
|
|||||||
else null;
|
else null;
|
||||||
if(activity != null) {
|
if(activity != null) {
|
||||||
if(activity is IWithResultLauncher)
|
if(activity is IWithResultLauncher)
|
||||||
StateApp.instance.requestDirectoryAccess(activity, "Grayjay Backup Directory", backupFiles.first.parent?.toUri()) {
|
StateApp.instance.requestDirectoryAccess(activity, "Grayjay Backup Directory", "Allows restoring of a backup", backupFiles.first?.parentFile?.uri) {
|
||||||
if(it != null) {
|
if(it != null) {
|
||||||
val customFiles = StateBackup.getAutomaticBackupDocumentFiles(activity, it);
|
val customFiles = StateBackup.getAutomaticBackupDocumentFiles(activity);
|
||||||
if(customFiles.first != null && customFiles.first!!.isFile && customFiles.first!!.exists() && customFiles.first!!.canRead())
|
if(customFiles.first != null && customFiles.first!!.isFile && customFiles.first!!.exists() && customFiles.first!!.canRead())
|
||||||
restoreAutomaticBackup(context, scope, password, ifExists, activity.contentResolver.openInputStream(customFiles.first!!.uri));
|
restoreAutomaticBackup(context, scope, password, ifExists);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ex: Throwable) {
|
catch (ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed main AutoBackup restore", ex)
|
Logger.e(TAG, "Failed main AutoBackup restore", ex)
|
||||||
if (!backupFiles.second.exists())
|
if (backupFiles.second?.exists() != true)
|
||||||
throw ex;
|
throw ex;
|
||||||
|
|
||||||
val backupBytesEncrypted = backupFiles.second.readBytes();
|
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
||||||
importZipBytes(context, scope, backupBytes);
|
importZipBytes(context, scope, backupBytes);
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
Logger.i(TAG, "Finished AutoBackup restore");
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
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.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
|
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
@@ -108,6 +112,11 @@ class StateDownloads {
|
|||||||
fun getPlaylistDownload(playlistId: String): PlaylistDownloadDescriptor? {
|
fun getPlaylistDownload(playlistId: String): PlaylistDownloadDescriptor? {
|
||||||
return _downloadPlaylists.findItem { it.id == playlistId };
|
return _downloadPlaylists.findItem { it.id == playlistId };
|
||||||
}
|
}
|
||||||
|
fun savePlaylistDownload(playlistDownload: PlaylistDownloadDescriptor) {
|
||||||
|
synchronized(playlistDownload.preventDownload) {
|
||||||
|
_downloadPlaylists.save(playlistDownload);
|
||||||
|
}
|
||||||
|
}
|
||||||
fun deleteCachedPlaylist(id: String) {
|
fun deleteCachedPlaylist(id: String) {
|
||||||
val pdl = getPlaylistDownload(id);
|
val pdl = getPlaylistDownload(id);
|
||||||
if(pdl != null)
|
if(pdl != null)
|
||||||
@@ -142,6 +151,19 @@ class StateDownloads {
|
|||||||
_downloading.delete(download);
|
_downloading.delete(download);
|
||||||
onDownloadsChanged.emit();
|
onDownloadsChanged.emit();
|
||||||
}
|
}
|
||||||
|
fun preventPlaylistDownload(download: VideoDownload) {
|
||||||
|
if(download.video != null && download.groupID != null && download.groupType == VideoDownload.GROUP_PLAYLIST) {
|
||||||
|
getPlaylistDownload(download.groupID!!)?.let {
|
||||||
|
synchronized(it.preventDownload) {
|
||||||
|
if(download.video?.url != null && !it.preventDownload.contains(download.video!!.url)) {
|
||||||
|
it.preventDownload.add(download.video!!.url);
|
||||||
|
savePlaylistDownload(it);
|
||||||
|
Logger.w(TAG, "Preventing further download attempts in playlist [${it.id}] for [${download.name}]:${download.video?.url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkForDownloadsTodos() {
|
fun checkForDownloadsTodos() {
|
||||||
val hasPlaylistChanged = checkForOutdatedPlaylists();
|
val hasPlaylistChanged = checkForOutdatedPlaylists();
|
||||||
@@ -157,12 +179,15 @@ class StateDownloads {
|
|||||||
val playlistsDownloaded = getCachedPlaylists();
|
val playlistsDownloaded = getCachedPlaylists();
|
||||||
for(playlist in playlistsDownloaded) {
|
for(playlist in playlistsDownloaded) {
|
||||||
val playlistDownload = getPlaylistDownload(playlist.playlist.id) ?: continue;
|
val playlistDownload = getPlaylistDownload(playlist.playlist.id) ?: continue;
|
||||||
|
val toIgnore = playlistDownload.getPreventDownloadList();
|
||||||
if(playlist.playlist.videos.any{ getCachedVideo(it.id) == null }) {
|
val missingVideoCount = playlist.playlist.videos.count { !toIgnore.contains(it.url) && getCachedVideo(it.id) == null };
|
||||||
Logger.i(TAG, "Found new videos on playlist [${playlist.playlist.name}]");
|
if(missingVideoCount > 0) {
|
||||||
|
Logger.i(TAG, "Found new videos (${missingVideoCount}) on playlist [${playlist.playlist.name}] to download");
|
||||||
continueDownload(playlistDownload, playlist.playlist);
|
continueDownload(playlistDownload, playlist.playlist);
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||||
}
|
}
|
||||||
return hasChanged;
|
return hasChanged;
|
||||||
}
|
}
|
||||||
@@ -171,6 +196,11 @@ class StateDownloads {
|
|||||||
var hasNew = false;
|
var hasNew = false;
|
||||||
for(item in playlist.videos) {
|
for(item in playlist.videos) {
|
||||||
val existing = getCachedVideo(item.id);
|
val existing = getCachedVideo(item.id);
|
||||||
|
|
||||||
|
if(!playlistDownload.shouldDownload(item)) {
|
||||||
|
Logger.i(TAG, "Not downloading for playlist [${playlistDownload.id}] Video [${item.name}]:${item.url}")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if(existing == null) {
|
if(existing == null) {
|
||||||
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
||||||
if(ongoingDownload != null) {
|
if(ongoingDownload != null) {
|
||||||
@@ -291,6 +321,32 @@ class StateDownloads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun downloadSubtitles(subtitle: ISubtitleSource, contentResolver: ContentResolver): SubtitleRawSource? {
|
||||||
|
val subtitleUri = subtitle.getSubtitlesURI();
|
||||||
|
if(subtitleUri == null)
|
||||||
|
return null;
|
||||||
|
var subtitles: String? = null;
|
||||||
|
if ("file" == subtitleUri.scheme) {
|
||||||
|
val inputStream = contentResolver.openInputStream(subtitleUri);
|
||||||
|
inputStream?.use { stream ->
|
||||||
|
val reader = stream.bufferedReader();
|
||||||
|
subtitles = reader.use { it.readText() };
|
||||||
|
}
|
||||||
|
} else if ("http" == subtitleUri.scheme || "https" == subtitleUri.scheme) {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val subtitleResponse = client.get(subtitleUri.toString());
|
||||||
|
if (!subtitleResponse.isOk) {
|
||||||
|
throw Exception("Cannot fetch subtitles from source '${subtitleUri}': ${subtitleResponse.code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitles = subtitleResponse.body?.toString()
|
||||||
|
?: throw Exception("Subtitles are invalid '${subtitleUri}': ${subtitleResponse.code}");
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError("Unsuported scheme");
|
||||||
|
}
|
||||||
|
return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.format, subtitles!!) else null;
|
||||||
|
}
|
||||||
|
|
||||||
fun cleanupDownloads(): Pair<Int, Long> {
|
fun cleanupDownloads(): Pair<Int, Long> {
|
||||||
val expected = getDownloadedVideos();
|
val expected = getDownloadedVideos();
|
||||||
val validFiles = HashSet(expected.flatMap { it.videoSource.map { it.filePath } + it.audioSource.map { it.filePath } });
|
val validFiles = HashSet(expected.flatMap { it.videoSource.map { it.filePath } + it.audioSource.map { it.filePath } });
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ class StatePlatform {
|
|||||||
//Pools always follow the behavior of the base client. So if user disables a plugin, it kills all pooled clients.
|
//Pools always follow the behavior of the base client. So if user disables a plugin, it kills all pooled clients.
|
||||||
//Each pooled client adds additional memory usage.
|
//Each pooled client adds additional memory usage.
|
||||||
//WARNING: Be careful with pooling some calls, as they might use the plugin subsequently afterwards. For example pagers might block plugins in future calls.
|
//WARNING: Be careful with pooling some calls, as they might use the plugin subsequently afterwards. For example pagers might block plugins in future calls.
|
||||||
private val _mainClientPool = PlatformMultiClientPool(2); //Used for all main user events, generally user critical
|
private val _mainClientPool = PlatformMultiClientPool("Main", 2); //Used for all main user events, generally user critical
|
||||||
private val _pagerClientPool = PlatformMultiClientPool(2); //Used primarily for calls that result in front-end pagers, preventing them from blocking other calls.
|
private val _pagerClientPool = PlatformMultiClientPool("Pagers", 2); //Used primarily for calls that result in front-end pagers, preventing them from blocking other calls.
|
||||||
private val _channelClientPool = PlatformMultiClientPool(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(1); //Used exclusively for playback trackers
|
private val _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers
|
||||||
private val _liveEventClientPool = PlatformMultiClientPool(1); //Used exclusively for live events
|
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
||||||
|
|
||||||
|
|
||||||
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
|
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
|
||||||
@@ -172,7 +172,11 @@ class StatePlatform {
|
|||||||
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
|
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
|
||||||
ImageVariable(plugin.config.absoluteIconUrl, null);
|
ImageVariable(plugin.config.absoluteIconUrl, null);
|
||||||
|
|
||||||
_availableClients.add(JSClient(context, plugin));
|
val client = JSClient(context, plugin);
|
||||||
|
client.onCaptchaException.subscribe { client, ex ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
}
|
||||||
|
_availableClients.add(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size)
|
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size)
|
||||||
@@ -287,6 +291,9 @@ class StatePlatform {
|
|||||||
StatePlugins.instance.getPlugin(id)
|
StatePlugins.instance.getPlugin(id)
|
||||||
?: throw IllegalStateException("Client existed, but plugin config didn't")
|
?: throw IllegalStateException("Client existed, but plugin config didn't")
|
||||||
);
|
);
|
||||||
|
newClient.onCaptchaException.subscribe { client, ex ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
if (_enabledClients.contains(client)) {
|
if (_enabledClients.contains(client)) {
|
||||||
@@ -399,13 +406,15 @@ class StatePlatform {
|
|||||||
return@async searchResult;
|
return@async searchResult;
|
||||||
} catch(ex: Throwable) {
|
} catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "getHomeRefresh", ex);
|
Logger.e(TAG, "getHomeRefresh", ex);
|
||||||
return@async null;
|
throw ex;
|
||||||
|
//return@async null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}.toList();
|
}.toList();
|
||||||
|
|
||||||
val finishedPager = deferred.map { it.second }.awaitFirstNotNullDeferred() ?: return EmptyPager();
|
val finishedPager = deferred.map { it.second }.awaitFirstNotNullDeferred() ?: return EmptyPager();
|
||||||
val toAwait = deferred.filter { it.second != finishedPager.first };
|
val toAwait = deferred.filter { it.second != finishedPager.first };
|
||||||
|
|
||||||
return RefreshDistributionContentPager(
|
return RefreshDistributionContentPager(
|
||||||
listOf(finishedPager.second),
|
listOf(finishedPager.second),
|
||||||
toAwait.map { it.second },
|
toAwait.map { it.second },
|
||||||
@@ -616,9 +625,13 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) };
|
fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) };
|
||||||
fun getChannelClient(url : String) : IPlatformClient = getChannelClientOrNull(url)
|
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})");
|
||||||
fun getChannelClientOrNull(url : String) : IPlatformClient? = getEnabledClients().find { it.isChannelUrl(url) };
|
fun getChannelClientOrNull(url : String, exclude: List<String>? = null) : IPlatformClient? =
|
||||||
|
if(exclude == null)
|
||||||
|
getEnabledClients().find { it.isChannelUrl(url) }
|
||||||
|
else
|
||||||
|
getEnabledClients().find { !exclude.contains(it.id) && it.isChannelUrl(url) };
|
||||||
|
|
||||||
fun getChannel(url: String, updateSubscriptions: Boolean = true): Deferred<IPlatformChannel> {
|
fun getChannel(url: String, updateSubscriptions: Boolean = true): Deferred<IPlatformChannel> {
|
||||||
Logger.i(TAG, "Platform - getChannel");
|
Logger.i(TAG, "Platform - getChannel");
|
||||||
@@ -629,9 +642,9 @@ class StatePlatform {
|
|||||||
return _scope.async { getChannelLive(url, updateSubscriptions) };
|
return _scope.async { getChannelLive(url, updateSubscriptions) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0): IPager<IPlatformContent> {
|
fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "Platform - getChannelVideos");
|
Logger.i(TAG, "Platform - getChannelVideos");
|
||||||
val baseClient = getChannelClient(channelUrl);
|
val baseClient = getChannelClient(channelUrl, ignorePlugins);
|
||||||
val clientCapabilities = baseClient.getChannelCapabilities();
|
val clientCapabilities = baseClient.getChannelCapabilities();
|
||||||
|
|
||||||
val client = if(usePooledClients > 1)
|
val client = if(usePooledClients > 1)
|
||||||
@@ -651,19 +664,24 @@ class StatePlatform {
|
|||||||
toQuery.add(ResultCapabilities.TYPE_STREAMS);
|
toQuery.add(ResultCapabilities.TYPE_STREAMS);
|
||||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE))
|
if(clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE))
|
||||||
toQuery.add(ResultCapabilities.TYPE_LIVE);
|
toQuery.add(ResultCapabilities.TYPE_LIVE);
|
||||||
|
if(clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS))
|
||||||
|
toQuery.add(ResultCapabilities.TYPE_POSTS);
|
||||||
|
|
||||||
if(isSubscriptionOptimized) {
|
if(isSubscriptionOptimized) {
|
||||||
val sub = StateSubscriptions.instance.getSubscription(channelUrl);
|
val sub = StateSubscriptions.instance.getSubscription(channelUrl);
|
||||||
if(sub != null) {
|
if(sub != null) {
|
||||||
val daysSinceLiveStream = sub.lastLiveStream.getNowDiffDays()
|
if(!sub.shouldFetchStreams()) {
|
||||||
if(daysSinceLiveStream > 7) {
|
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
|
||||||
Logger.i(TAG, "Subscription [${channelUrl}] Last livestream > 7 days, skipping live streams [${daysSinceLiveStream} days ago]");
|
|
||||||
toQuery.remove(ResultCapabilities.TYPE_LIVE);
|
toQuery.remove(ResultCapabilities.TYPE_LIVE);
|
||||||
}
|
}
|
||||||
if(daysSinceLiveStream > 14) {
|
if(!sub.shouldFetchLiveStreams()) {
|
||||||
Logger.i(TAG, "Subscription [${channelUrl}] Last livestream > 15 days, skipping streams [${daysSinceLiveStream} days ago]");
|
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 15 days, skipping streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
|
||||||
toQuery.remove(ResultCapabilities.TYPE_STREAMS);
|
toQuery.remove(ResultCapabilities.TYPE_STREAMS);
|
||||||
}
|
}
|
||||||
|
if(!sub.shouldFetchPosts()) {
|
||||||
|
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 5 days, skipping posts [${sub.lastPost.getNowDiffDays()} days ago]");
|
||||||
|
toQuery.remove(ResultCapabilities.TYPE_POSTS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class StatePlayer {
|
|||||||
private val MIN_BUFFER_DURATION = 10000;
|
private val MIN_BUFFER_DURATION = 10000;
|
||||||
private val MAX_BUFFER_DURATION = 60000;
|
private val MAX_BUFFER_DURATION = 60000;
|
||||||
private val MIN_PLAYBACK_START_BUFFER = 500;
|
private val MIN_PLAYBACK_START_BUFFER = 500;
|
||||||
private val MIN_PLAYBACK_RESUME_BUFFER = 1000;
|
private val MIN_PLAYBACK_RESUME_BUFFER = 2500;
|
||||||
private val BUFFER_SIZE = 1024 * 64;
|
private val BUFFER_SIZE = 1024 * 64;
|
||||||
|
|
||||||
var isOpen : Boolean = false
|
var isOpen : Boolean = false
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -265,6 +266,12 @@ class StatePlaylists {
|
|||||||
builder.messages.add("${name}:[${it}] is no longer available");
|
builder.messages.add("${name}:[${it}] is no longer available");
|
||||||
return@map null;
|
return@map null;
|
||||||
}
|
}
|
||||||
|
catch(ex: NoPlatformClientException) {
|
||||||
|
throw ReconstructionException(name, "No source enabled for [${it}]", ex);
|
||||||
|
//TODO: Propagate this to dialog, and then back, allowing users to enable plugins...
|
||||||
|
//builder.messages.add("No source enabled for [${it}]");
|
||||||
|
//return@map null;
|
||||||
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
|
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -27,12 +28,20 @@ class StatePlugins {
|
|||||||
private val TAG = "StatePlugins";
|
private val TAG = "StatePlugins";
|
||||||
|
|
||||||
private val FORCE_REINSTALL_EMBEDDED = false;
|
private val FORCE_REINSTALL_EMBEDDED = false;
|
||||||
|
private var _isFirstEmbedUpdate = true;
|
||||||
|
|
||||||
private val _pluginScripts = FragmentedStorage.getDirectory<PluginScriptsDirectory>();
|
private val _pluginScripts = FragmentedStorage.getDirectory<PluginScriptsDirectory>();
|
||||||
private var _plugins = FragmentedStorage.storeJson<SourcePluginDescriptor>("plugins")
|
private var _plugins = FragmentedStorage.storeJson<SourcePluginDescriptor>("plugins")
|
||||||
.load();
|
.load();
|
||||||
private val iconsDir = FragmentedStorage.getDirectory<PluginIconStorage>();
|
private val iconsDir = FragmentedStorage.getDirectory<PluginIconStorage>();
|
||||||
|
|
||||||
|
|
||||||
|
private val _syncObject = Object()
|
||||||
|
private var _embeddedSources: Map<String, String>? = null
|
||||||
|
private var _embeddedSourcesDefault: List<String>? = null
|
||||||
|
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
||||||
|
|
||||||
|
|
||||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||||
if(iconsDir.hasIcon(id))
|
if(iconsDir.hasIcon(id))
|
||||||
return iconsDir.getIconBinary(id);
|
return iconsDir.getIconBinary(id);
|
||||||
@@ -53,18 +62,6 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class PluginConfig(
|
|
||||||
val SOURCES_EMBEDDED: Map<String, String>,
|
|
||||||
val SOURCES_EMBEDDED_DEFAULT: List<String>,
|
|
||||||
val SOURCES_UNDER_CONSTRUCTION: Map<String, String>
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _syncObject = Object()
|
|
||||||
private var _embeddedSources: Map<String, String>? = null
|
|
||||||
private var _embeddedSourcesDefault: List<String>? = null
|
|
||||||
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
|
||||||
|
|
||||||
private fun ensureSourcesConfigLoaded(context: Context) {
|
private fun ensureSourcesConfigLoaded(context: Context) {
|
||||||
if (_embeddedSources != null && _embeddedSourcesDefault != null && _sourcesUnderConstruction != null) {
|
if (_embeddedSources != null && _embeddedSourcesDefault != null && _sourcesUnderConstruction != null) {
|
||||||
return
|
return
|
||||||
@@ -122,8 +119,11 @@ class StatePlugins {
|
|||||||
Logger.i(TAG, "Found outdated embedded plugin [${existing.config.id}] ${existing.config.name}, deleting and reinstalling");
|
Logger.i(TAG, "Found outdated embedded plugin [${existing.config.id}] ${existing.config.name}, deleting and reinstalling");
|
||||||
deletePlugin(embedded.key);
|
deletePlugin(embedded.key);
|
||||||
}
|
}
|
||||||
|
else if(existing != null && _isFirstEmbedUpdate)
|
||||||
|
Logger.i(TAG, "Embedded plugin [${existing.config.id}] ${existing.config.name}, up to date (${existing.config.version} >= ${embeddedConfig?.version})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_isFirstEmbedUpdate = false;
|
||||||
}
|
}
|
||||||
fun installMissingEmbeddedPlugins(context: Context) {
|
fun installMissingEmbeddedPlugins(context: Context) {
|
||||||
val plugins = getPlugins();
|
val plugins = getPlugins();
|
||||||
@@ -373,7 +373,7 @@ class StatePlugins {
|
|||||||
if(icon != null)
|
if(icon != null)
|
||||||
iconsDir.saveIconBinary(config.id, icon);
|
iconsDir.saveIconBinary(config.id, icon);
|
||||||
|
|
||||||
_plugins.save(SourcePluginDescriptor(config, null, flags));
|
_plugins.save(SourcePluginDescriptor(config, null, null, flags));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
@@ -408,6 +408,18 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPluginCaptcha(id: String, captcha: SourceCaptchaData?) {
|
||||||
|
if(id == StateDeveloper.DEV_ID) {
|
||||||
|
StatePlatform.instance.getDevClient()?.let {
|
||||||
|
it.setCaptcha(captcha);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
val descriptor = getPlugin(id) ?: throw IllegalArgumentException("Plugin [${id}] does not exist");
|
||||||
|
descriptor.updateCaptcha(captcha);
|
||||||
|
_plugins.save(descriptor);
|
||||||
|
}
|
||||||
fun setPluginAuth(id: String, auth: SourceAuth?) {
|
fun setPluginAuth(id: String, auth: SourceAuth?) {
|
||||||
if(id == StateDeveloper.DEV_ID) {
|
if(id == StateDeveloper.DEV_ID) {
|
||||||
StatePlatform.instance.getDevClient()?.let {
|
StatePlatform.instance.getDevClient()?.let {
|
||||||
@@ -422,6 +434,13 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class PluginConfig(
|
||||||
|
val SOURCES_EMBEDDED: Map<String, String>,
|
||||||
|
val SOURCES_EMBEDDED_DEFAULT: List<String>,
|
||||||
|
val SOURCES_UNDER_CONSTRUCTION: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _instance : StatePlugins? = null;
|
private var _instance : StatePlugins? = null;
|
||||||
val instance : StatePlugins
|
val instance : StatePlugins
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class StatePolycentric {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent> {
|
fun getChannelContent(profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
||||||
//TODO: Currently abusing subscription concurrency for parallelism
|
//TODO: Currently abusing subscription concurrency for parallelism
|
||||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||||
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
||||||
@@ -138,7 +138,7 @@ class StatePolycentric {
|
|||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return@mapNotNull StatePlatform.instance.getChannelContent(url, isSubscriptionOptimized, concurrency);
|
return@mapNotNull StatePlatform.instance.getChannelContent(url, isSubscriptionOptimized, concurrency, ignorePlugins);
|
||||||
}.toTypedArray();
|
}.toTypedArray();
|
||||||
|
|
||||||
val pager = MultiChronoContentPager(pagers);
|
val pager = MultiChronoContentPager(pagers);
|
||||||
@@ -302,7 +302,7 @@ class StatePolycentric {
|
|||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
rating = RatingLikeDislikes(likes, dislikes),
|
rating = RatingLikeDislikes(likes, dislikes),
|
||||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
replyCount = replies.toInt(),
|
replyCount = replies.toInt(),
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package com.futo.platformplayer.states
|
|||||||
|
|
||||||
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.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
@@ -12,9 +15,12 @@ import com.futo.platformplayer.constructs.Event0
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
|
import com.futo.platformplayer.getNowDiffDays
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
@@ -67,7 +73,7 @@ class StateSubscriptions {
|
|||||||
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
||||||
}
|
}
|
||||||
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null) {
|
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null) {
|
||||||
Logger.i(TAG, "updateSubscriptionFeed");
|
Logger.v(TAG, "updateSubscriptionFeed");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(_globalSubscriptionsLock) {
|
||||||
if (isGlobalUpdating || (onlyIfNull && _globalSubscriptionFeed != null)) {
|
if (isGlobalUpdating || (onlyIfNull && _globalSubscriptionFeed != null)) {
|
||||||
@@ -216,13 +222,38 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeed(allowFailure: Boolean = false): MultiChronoContentPager {
|
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
val pluginReqCounts = mutableMapOf<JSClient, Int>();
|
||||||
|
|
||||||
|
for(sub in subs) {
|
||||||
|
val client = StatePlatform.instance.getChannelClientOrNull(sub.channel.url);
|
||||||
|
if(client !is JSClient)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
val channelCaps = client.getChannelCapabilities();
|
||||||
|
if(!pluginReqCounts.containsKey(client))
|
||||||
|
pluginReqCounts[client] = 1;
|
||||||
|
else
|
||||||
|
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||||
|
|
||||||
|
if(channelCaps.hasType(ResultCapabilities.TYPE_STREAMS) && sub.shouldFetchStreams())
|
||||||
|
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||||
|
if(channelCaps.hasType(ResultCapabilities.TYPE_LIVE) && sub.shouldFetchLiveStreams())
|
||||||
|
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||||
|
if(channelCaps.hasType(ResultCapabilities.TYPE_POSTS) && sub.shouldFetchPosts())
|
||||||
|
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||||
|
}
|
||||||
|
return pluginReqCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager<IPlatformContent> {
|
||||||
val result = getSubscriptionsFeedWithExceptions(allowFailure, true);
|
val result = getSubscriptionsFeedWithExceptions(allowFailure, true);
|
||||||
if(result.second.any())
|
if(result.second.any())
|
||||||
throw result.second.first();
|
throw result.second.first();
|
||||||
return result.first;
|
return result.first;
|
||||||
}
|
}
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<MultiChronoContentPager, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val subsPager: Array<IPager<IPlatformContent>>;
|
val subsPager: Array<IPager<IPlatformContent>>;
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
@@ -230,8 +261,11 @@ class StateSubscriptions {
|
|||||||
var finished = 0;
|
var finished = 0;
|
||||||
val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf();
|
val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf();
|
||||||
val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency();
|
val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency();
|
||||||
|
val failedPlugins = arrayListOf<String>();
|
||||||
for (sub in getSubscriptions().filter { StatePlatform.instance.hasEnabledChannelClient(it.channel.url) }) {
|
for (sub in getSubscriptions().filter { StatePlatform.instance.hasEnabledChannelClient(it.channel.url) }) {
|
||||||
tasks.add(_subscriptionsPool.submit<Pair<Subscription, IPager<IPlatformContent>?>> {
|
tasks.add(_subscriptionsPool.submit<Pair<Subscription, IPager<IPlatformContent>?>> {
|
||||||
|
val toIgnore = synchronized(failedPlugins){ failedPlugins.toList() };
|
||||||
|
|
||||||
var polycentricProfile : PolycentricCache.CachedPolycentricProfile? = null;
|
var polycentricProfile : PolycentricCache.CachedPolycentricProfile? = null;
|
||||||
val getProfileTime = measureTimeMillis {
|
val getProfileTime = measureTimeMillis {
|
||||||
try {
|
try {
|
||||||
@@ -258,9 +292,9 @@ class StateSubscriptions {
|
|||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
val profile = polycentricProfile?.profile
|
val profile = polycentricProfile?.profile
|
||||||
pager = if (profile != null)
|
pager = if (profile != null)
|
||||||
StatePolycentric.instance.getChannelContent(profile, true, concurrency)
|
StatePolycentric.instance.getChannelContent(profile, true, concurrency, toIgnore)
|
||||||
else
|
else
|
||||||
StatePlatform.instance.getChannelContent(sub.channel.url, true, concurrency);
|
StatePlatform.instance.getChannelContent(sub.channel.url, true, concurrency, toIgnore);
|
||||||
|
|
||||||
if (cacheScope != null)
|
if (cacheScope != null)
|
||||||
pager = ChannelContentCache.cachePagerResults(cacheScope, pager) {
|
pager = ChannelContentCache.cachePagerResults(cacheScope, pager) {
|
||||||
@@ -276,12 +310,31 @@ class StateSubscriptions {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Subscription [${sub.channel.name}] failed", ex);
|
||||||
finished++;
|
finished++;
|
||||||
onProgress?.invoke(finished, tasks.size);
|
onProgress?.invoke(finished, tasks.size);
|
||||||
val channelEx = ChannelException(sub.channel, ex);
|
val channelEx = ChannelException(sub.channel, ex);
|
||||||
synchronized(exceptionMap) {
|
synchronized(exceptionMap) {
|
||||||
exceptionMap.put(sub, channelEx);
|
exceptionMap.put(sub, channelEx);
|
||||||
}
|
}
|
||||||
|
if(ex is ScriptCaptchaRequiredException) {
|
||||||
|
synchronized(failedPlugins) {
|
||||||
|
//Fail all subscription calls to plugin if it has a captcha issue
|
||||||
|
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||||
|
Logger.w(TAG, "Subscriptionsgnoring plugin [${ex.config.name}] due to Captcha");
|
||||||
|
failedPlugins.add(ex.config.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(ex is ScriptCriticalException) {
|
||||||
|
synchronized(failedPlugins) {
|
||||||
|
//Fail all subscription calls to plugin if it has a critical issue
|
||||||
|
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||||
|
Logger.w(TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message);
|
||||||
|
failedPlugins.add(ex.config.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if(!withCacheFallback)
|
if(!withCacheFallback)
|
||||||
throw channelEx;
|
throw channelEx;
|
||||||
else {
|
else {
|
||||||
@@ -327,9 +380,10 @@ class StateSubscriptions {
|
|||||||
throw exs.first();
|
throw exs.first();
|
||||||
|
|
||||||
Logger.i(TAG, "Subscription pager with ${subsPager.size} channels");
|
Logger.i(TAG, "Subscription pager with ${subsPager.size} channels");
|
||||||
val pager = MultiChronoContentPager(subsPager, allowFailure);
|
val pager = MultiChronoContentPager(subsPager, allowFailure, 15);
|
||||||
pager.initialize();
|
pager.initialize();
|
||||||
return Pair(pager, exs);
|
//return Pair(pager, exs);
|
||||||
|
return Pair(DedupContentPager(pager), exs);
|
||||||
}
|
}
|
||||||
|
|
||||||
//New Migration
|
//New Migration
|
||||||
|
|||||||
@@ -37,14 +37,13 @@ class StateTelemetry {
|
|||||||
BuildConfig.BUILD_TYPE,
|
BuildConfig.BUILD_TYPE,
|
||||||
BuildConfig.DEBUG,
|
BuildConfig.DEBUG,
|
||||||
BuildConfig.IS_UNSTABLE_BUILD,
|
BuildConfig.IS_UNSTABLE_BUILD,
|
||||||
Instant.now().epochSecond,
|
|
||||||
Build.BRAND,
|
Build.BRAND,
|
||||||
Build.MANUFACTURER,
|
Build.MANUFACTURER,
|
||||||
Build.MODEL
|
Build.MODEL
|
||||||
);
|
);
|
||||||
|
|
||||||
val headers = hashMapOf(
|
val headers = hashMapOf(
|
||||||
"Content-Type" to "text/plain"
|
"Content-Type" to "application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
val json = Json.encodeToString(telemetry);
|
val json = Json.encodeToString(telemetry);
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class ManagedStore<T>{
|
|||||||
val builder = ReconstructStore.Builder();
|
val builder = ReconstructStore.Builder();
|
||||||
|
|
||||||
for (recon in items) {
|
for (recon in items) {
|
||||||
|
onProgress?.invoke(0, total);
|
||||||
//Retry once
|
//Retry once
|
||||||
for (i in 0 .. 1) {
|
for (i in 0 .. 1) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.graphics.drawable.Animatable
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
|
||||||
class Loader : LinearLayout {
|
class Loader : LinearLayout {
|
||||||
@@ -15,7 +17,7 @@ class Loader : LinearLayout {
|
|||||||
private val _animatable: Animatable;
|
private val _animatable: Animatable;
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_loader, this, true);
|
inflate(context, R.layout.view_loader, this);
|
||||||
_imageLoader = findViewById(R.id.image_loader);
|
_imageLoader = findViewById(R.id.image_loader);
|
||||||
_animatable = _imageLoader.drawable as Animatable;
|
_animatable = _imageLoader.drawable as Animatable;
|
||||||
|
|
||||||
@@ -29,6 +31,18 @@ class Loader : LinearLayout {
|
|||||||
|
|
||||||
visibility = View.GONE;
|
visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
constructor(context: Context, automatic: Boolean, height: Int = -1) : super(context) {
|
||||||
|
inflate(context, R.layout.view_loader, this);
|
||||||
|
_imageLoader = findViewById(R.id.image_loader);
|
||||||
|
_animatable = _imageLoader.drawable as Animatable;
|
||||||
|
_automatic = automatic;
|
||||||
|
|
||||||
|
if(height > 0) {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility = View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user