mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73321ee362 | |||
| 182c88fc9e | |||
| 9d39d74be5 | |||
| d8d8d6f666 | |||
| df0504cead | |||
| 851b547d64 | |||
| f49ecf1159 | |||
| 081ae1dd88 | |||
| 374d9950be | |||
| 9ffdf39f13 | |||
| 8bb1ff87c0 |
@@ -127,6 +127,10 @@
|
||||
android:name=".activities.ExceptionActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.CaptchaActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.LoginActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
||||
@@ -64,6 +64,14 @@ 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 UnavailableException extends ScriptException {
|
||||
constructor(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,6 @@ import android.content.ContentResolver
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.sources.IAudioUrlSource
|
||||
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.models.Playlist
|
||||
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.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
@@ -29,7 +29,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class UISlideOverlays {
|
||||
companion object {
|
||||
@@ -45,7 +45,7 @@ class UISlideOverlays {
|
||||
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>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
||||
@@ -68,6 +68,12 @@ class UISlideOverlays {
|
||||
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,
|
||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
||||
selectedVideo = null;
|
||||
@@ -76,7 +82,7 @@ class UISlideOverlays {
|
||||
menu?.setOk("Download");
|
||||
}, false)) +
|
||||
videoSources
|
||||
.filter { it is IVideoUrlSource }
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideo = it as IVideoUrlSource;
|
||||
@@ -88,14 +94,14 @@ class UISlideOverlays {
|
||||
));
|
||||
|
||||
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(),
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
||||
|
||||
|
||||
audioSources?.let { audioSources ->
|
||||
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
||||
.filter { it is IAudioUrlSource }
|
||||
.filter { VideoHelper.isDownloadable(it) }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||
selectedAudio = it as IAudioUrlSource;
|
||||
@@ -111,24 +117,27 @@ class UISlideOverlays {
|
||||
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,
|
||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||
if (selectedSubtitle == it) {
|
||||
selectedSubtitle = null;
|
||||
menu?.selectOption(subtitleSources, null);
|
||||
} else {
|
||||
selectedSubtitle = it;
|
||||
menu?.selectOption(subtitleSources, it);
|
||||
}
|
||||
}, false);
|
||||
}));
|
||||
//ContentResolver is required for subtitles..
|
||||
if(contentResolver != null) {
|
||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||
if (selectedSubtitle == it) {
|
||||
selectedSubtitle = null;
|
||||
menu?.selectOption(subtitleSources, null);
|
||||
} else {
|
||||
selectedSubtitle = it;
|
||||
menu?.selectOption(subtitleSources, it);
|
||||
}
|
||||
}, false);
|
||||
}));
|
||||
}
|
||||
|
||||
menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items);
|
||||
|
||||
@@ -153,29 +162,12 @@ class UISlideOverlays {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
||||
if (subtitleUri != 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 Exception("Unsuported scheme");
|
||||
}
|
||||
//TODO: Remove uri dependency, should be able to work with raw aswell?
|
||||
if (subtitleUri != null && contentResolver != null) {
|
||||
val subtitlesRaw = StateDownloads.instance.downloadSubtitles(subtitleToDownload, contentResolver);
|
||||
|
||||
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 {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -191,10 +183,41 @@ class UISlideOverlays {
|
||||
};
|
||||
return menu.apply { show() };
|
||||
}
|
||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup) {
|
||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
||||
StateDownloads.instance.download(video, px, bitrate)
|
||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
||||
val handleUnknownDownload: ()->Unit = {
|
||||
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) {
|
||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
||||
@@ -269,6 +292,18 @@ class UISlideOverlays {
|
||||
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 {
|
||||
val items = arrayListOf<View>();
|
||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||
@@ -291,7 +326,7 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
||||
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
||||
{ showDownloadVideoOverlay(video, container); }, false)
|
||||
{ showDownloadVideoOverlay(video, container, true); }, false)
|
||||
))
|
||||
items.add(
|
||||
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",
|
||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
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>();
|
||||
|
||||
@@ -58,7 +58,7 @@ fun findNonRuntimeException(ex: Throwable?): Throwable? {
|
||||
|
||||
fun warnIfMainThread(context: String) {
|
||||
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() {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,6 +607,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
val name = when(type) {
|
||||
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
||||
else -> type
|
||||
@@ -894,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 requestCode: Int? = -1;
|
||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
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 id: PlatformID = PlatformID("", null, pluginId);
|
||||
override val name: String = "";
|
||||
@@ -12,4 +12,5 @@ class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
|
||||
override val shareUrl: String = "";
|
||||
override val datetime: OffsetDateTime? = null;
|
||||
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
|
||||
val error: Throwable? = exception
|
||||
}
|
||||
@@ -15,29 +15,36 @@ class DevJSClient : JSClient {
|
||||
|
||||
private val _devScript: String;
|
||||
private var _auth: SourceAuth? = null;
|
||||
private var _captcha: SourceCaptchaData? = null;
|
||||
|
||||
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;
|
||||
_auth = auth;
|
||||
_captcha = captcha;
|
||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||
}
|
||||
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;
|
||||
_auth = auth;
|
||||
_captcha = captcha;
|
||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||
}
|
||||
|
||||
fun setCaptcha(captcha: SourceCaptchaData? = null) {
|
||||
_captcha = captcha;
|
||||
}
|
||||
fun setAuth(auth: SourceAuth? = null) {
|
||||
_auth = auth;
|
||||
}
|
||||
fun recreate(context: Context): DevJSClient {
|
||||
return DevJSClient(context, config, _devScript, _auth, devID);
|
||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
|
||||
}
|
||||
|
||||
override fun getCopy(): JSClient {
|
||||
return DevJSClient(_context, descriptor, _script, _auth, saveState(), devID);
|
||||
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
|
||||
@@ -25,7 +25,11 @@ import com.futo.platformplayer.api.media.platforms.js.internal.*
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.*
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
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.ScriptValidationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -59,6 +63,7 @@ open class JSClient : IPlatformClient {
|
||||
private var _enabled: Boolean = false;
|
||||
|
||||
private val _auth: SourceAuth?;
|
||||
private val _captcha: SourceCaptchaData?;
|
||||
|
||||
private val _injectedSaveState: String?;
|
||||
|
||||
@@ -85,6 +90,7 @@ open class JSClient : IPlatformClient {
|
||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||
|
||||
val onDisabled = Event1<JSClient>();
|
||||
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
||||
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||
this._context = context;
|
||||
@@ -93,10 +99,11 @@ open class JSClient : IPlatformClient {
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
_auth = descriptor.getAuth();
|
||||
_captcha = descriptor.getCaptchaData();
|
||||
flags = descriptor.flags.toTypedArray();
|
||||
|
||||
_client = JSHttpClient(this);
|
||||
_clientAuth = JSHttpClient(this, _auth);
|
||||
_client = JSHttpClient(this, null, _captcha);
|
||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||
_plugin.withDependency(context, "scripts/source.js");
|
||||
@@ -108,6 +115,11 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
else
|
||||
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) {
|
||||
this._context = context;
|
||||
@@ -116,15 +128,21 @@ open class JSClient : IPlatformClient {
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
_auth = descriptor.getAuth();
|
||||
_captcha = descriptor.getCaptchaData();
|
||||
flags = descriptor.flags.toTypedArray();
|
||||
|
||||
_client = JSHttpClient(this);
|
||||
_clientAuth = JSHttpClient(this, _auth);
|
||||
_client = JSHttpClient(this, null, _captcha);
|
||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||
_plugin.withDependency(context, "scripts/source.js");
|
||||
_plugin.withScript(script);
|
||||
_script = script;
|
||||
|
||||
_plugin.onScriptException.subscribe {
|
||||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
}
|
||||
|
||||
open fun getCopy(): JSClient {
|
||||
@@ -561,11 +579,13 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||
if(ex is PluginEngineException)
|
||||
return;
|
||||
try {
|
||||
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
|
||||
"Plugin ${config.name} encountered an error in [${method}]",
|
||||
"${ex.message}\nPlease contact the plugin developer",
|
||||
AnnouncementType.RECURRING,
|
||||
AnnouncementType.SESSION_RECURRING,
|
||||
OffsetDateTime.now());
|
||||
}
|
||||
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,6 +35,7 @@ class SourcePluginConfig(
|
||||
|
||||
val settings: List<Setting> = listOf(),
|
||||
|
||||
var captcha: SourcePluginCaptchaConfig? = null,
|
||||
val authentication: SourcePluginAuthConfig? = null,
|
||||
var sourceUrl: String? = null,
|
||||
val constants: HashMap<String, String> = hashMapOf(),
|
||||
|
||||
+16
-3
@@ -13,22 +13,28 @@ class SourcePluginDescriptor {
|
||||
|
||||
var appSettings: AppPluginSettings = AppPluginSettings();
|
||||
|
||||
var authEncrypted: String?
|
||||
var authEncrypted: String? = null
|
||||
private set;
|
||||
var captchaEncrypted: String? = null
|
||||
private set;
|
||||
|
||||
val flags: List<String>;
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
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.authEncrypted = authEncrypted;
|
||||
this.captchaEncrypted = captchaEncrypted;
|
||||
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.authEncrypted = authEncrypted;
|
||||
this.captchaEncrypted = captchaEncrypted;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@@ -41,6 +47,13 @@ class SourcePluginDescriptor {
|
||||
return map;
|
||||
}
|
||||
|
||||
fun updateCaptcha(captcha: SourceCaptchaData?) {
|
||||
captchaEncrypted = captcha?.toEncrypted();
|
||||
onCaptchaChanged.emit();
|
||||
}
|
||||
fun getCaptchaData(): SourceCaptchaData? {
|
||||
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||
}
|
||||
|
||||
fun updateAuth(str: SourceAuth?) {
|
||||
authEncrypted = str?.toEncrypted();
|
||||
|
||||
+32
-24
@@ -5,65 +5,72 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
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.SourceCaptchaData
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
|
||||
class JSHttpClient : ManagedHttpClient {
|
||||
private val _jsClient: JSClient?;
|
||||
private val _auth: SourceAuth?;
|
||||
private val _captcha: SourceCaptchaData?;
|
||||
|
||||
var doUpdateCookies: Boolean = true;
|
||||
var doApplyCookies: Boolean = true;
|
||||
var doAllowNewCookies: Boolean = true;
|
||||
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;
|
||||
_auth = auth;
|
||||
_captcha = captcha;
|
||||
|
||||
_currentCookieMap = hashMapOf();
|
||||
if(!auth?.cookieMap.isNullOrEmpty()) {
|
||||
_currentCookieMap = hashMapOf();
|
||||
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!!)
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun clone(): ManagedHttpClient {
|
||||
val newClient = JSHttpClient(_jsClient, _auth);
|
||||
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
|
||||
null;
|
||||
hashMapOf();
|
||||
return newClient;
|
||||
}
|
||||
|
||||
override fun beforeRequest(request: Request) {
|
||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
||||
|
||||
val auth = _auth;
|
||||
if (auth != null) {
|
||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
||||
|
||||
//TODO: Possibly add doApplyHeaders
|
||||
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
||||
request.headers[header.key] = header.value;
|
||||
}
|
||||
|
||||
if(doApplyCookies) {
|
||||
if (!_currentCookieMap.isNullOrEmpty()) {
|
||||
val cookiesToApply = hashMapOf<String, String>();
|
||||
synchronized(_currentCookieMap!!) {
|
||||
for(cookie in _currentCookieMap!!
|
||||
.filter { domain.matchesDomain(it.key) }
|
||||
.flatMap { it.value.toList() })
|
||||
cookiesToApply[cookie.first] = cookie.second;
|
||||
};
|
||||
if(doApplyCookies) {
|
||||
if (!_currentCookieMap.isNullOrEmpty()) {
|
||||
val cookiesToApply = hashMapOf<String, String>();
|
||||
synchronized(_currentCookieMap!!) {
|
||||
for(cookie in _currentCookieMap!!
|
||||
.filter { domain.matchesDomain(it.key) }
|
||||
.flatMap { it.value.toList() })
|
||||
cookiesToApply[cookie.first] = cookie.second;
|
||||
};
|
||||
|
||||
if(cookiesToApply.size > 0) {
|
||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||
request.headers["Cookie"] = cookieString;
|
||||
}
|
||||
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
||||
if(cookiesToApply.size > 0) {
|
||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||
request.headers["Cookie"] = cookieString;
|
||||
}
|
||||
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +87,7 @@ class JSHttpClient : ManagedHttpClient {
|
||||
val defaultCookieDomain =
|
||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
for (header in resp.headers) {
|
||||
if (_currentCookieMap != null && header.key.lowercase() == "set-cookie") {
|
||||
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.key.lowercase() == "set-cookie") {
|
||||
val newCookies = cookieStringToMap(header.value);
|
||||
for (cookie in newCookies) {
|
||||
val endIndex = cookie.value.indexOf(";");
|
||||
@@ -155,4 +162,5 @@ class JSHttpClient : ManagedHttpClient {
|
||||
|
||||
Logger.i("Testing", code);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-4
@@ -9,7 +9,7 @@ import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
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 codec: String = "HLS";
|
||||
override val name : String;
|
||||
@@ -31,9 +31,6 @@ class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSou
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
|
||||
override fun getAudioUrl(): String {
|
||||
return url;
|
||||
}
|
||||
|
||||
companion object {
|
||||
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.getOrThrow
|
||||
|
||||
class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
||||
class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||
override val width : Int = 0;
|
||||
override val height : Int = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
override fun getVideoUrl(): String {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -1,5 +1,6 @@
|
||||
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.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -37,8 +38,12 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
||||
synchronized(_pending) {
|
||||
_pending.remove(pendingPager);
|
||||
}
|
||||
if(error != null)
|
||||
if(error != null) {
|
||||
onPagerError.emit(error);
|
||||
val replacing = _placeHolderPagersPaired[pendingPager];
|
||||
if(replacing != null)
|
||||
updatePager(null, replacing, error);
|
||||
}
|
||||
else
|
||||
updatePager(pendingPager.getCompleted());
|
||||
}
|
||||
@@ -60,9 +65,17 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
||||
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
|
||||
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
||||
|
||||
private fun updatePager(pagerToAdd: IPager<T>?) {
|
||||
if(pagerToAdd == null)
|
||||
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
||||
if(pagerToAdd == null) {
|
||||
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||
_currentPager = PlaceholderPager(5) {
|
||||
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||
} as IPager<T>;
|
||||
onPagerChanged.emit(_currentPager);
|
||||
}
|
||||
return;
|
||||
}
|
||||
synchronized(_pagersReusable) {
|
||||
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
||||
_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.
|
||||
*/
|
||||
class PlaceholderPager : IPager<IPlatformContent> {
|
||||
private val _creator: ()->IPlatformContent;
|
||||
val placeholderFactory: ()->IPlatformContent;
|
||||
private val _pageSize: Int;
|
||||
|
||||
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
|
||||
_creator = placeholderCreator;
|
||||
placeholderFactory = placeholderCreator;
|
||||
_pageSize = pageSize;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class PlaceholderPager : IPager<IPlatformContent> {
|
||||
override fun getResults(): List<IPlatformContent> {
|
||||
val pages = ArrayList<IPlatformContent>();
|
||||
for(item in 1.._pageSize)
|
||||
pages.add(_creator());
|
||||
pages.add(placeholderFactory());
|
||||
return pages;
|
||||
}
|
||||
override fun hasMorePages(): Boolean = true;
|
||||
|
||||
@@ -8,6 +8,10 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
||||
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
|
||||
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
|
||||
|
||||
fun hasListeners(): Boolean =
|
||||
synchronized(_listeners){_listeners.isNotEmpty()} ||
|
||||
synchronized(_conditionalListeners){_conditionalListeners.isNotEmpty()};
|
||||
|
||||
fun subscribeConditional(listener: ConditionalHandler) {
|
||||
synchronized(_conditionalListeners) {
|
||||
_conditionalListeners.add(TaggedHandler(listener));
|
||||
@@ -65,10 +69,7 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
||||
|
||||
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
||||
fun emit() : Boolean {
|
||||
var handled: Boolean;
|
||||
synchronized(_listeners) {
|
||||
handled = _listeners.isNotEmpty();
|
||||
}
|
||||
var handled = false;
|
||||
|
||||
synchronized(_conditionalListeners) {
|
||||
for (conditional in _conditionalListeners)
|
||||
@@ -76,6 +77,7 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
||||
}
|
||||
|
||||
synchronized(_listeners) {
|
||||
handled = handled || _listeners.isNotEmpty();
|
||||
for (handler in _listeners)
|
||||
handler.handler.invoke();
|
||||
}
|
||||
@@ -85,17 +87,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
||||
}
|
||||
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
||||
fun emit(value : T1): Boolean {
|
||||
var handled: Boolean;
|
||||
synchronized(_listeners) {
|
||||
handled = _listeners.isNotEmpty();
|
||||
}
|
||||
|
||||
var handled = false;
|
||||
synchronized(_conditionalListeners) {
|
||||
for (conditional in _conditionalListeners)
|
||||
handled = handled || conditional.handler.invoke(value);
|
||||
}
|
||||
|
||||
synchronized(_listeners) {
|
||||
handled = handled || _listeners.isNotEmpty();
|
||||
for (handler in _listeners)
|
||||
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)>() {
|
||||
fun emit(value1 : T1, value2 : T2): Boolean {
|
||||
var handled: Boolean;
|
||||
synchronized(_listeners) {
|
||||
handled = _listeners.isNotEmpty();
|
||||
}
|
||||
var handled = false;
|
||||
|
||||
synchronized(_conditionalListeners) {
|
||||
for (conditional in _conditionalListeners)
|
||||
@@ -116,6 +112,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
||||
}
|
||||
|
||||
synchronized(_listeners) {
|
||||
handled = handled || _listeners.isNotEmpty();
|
||||
for (handler in _listeners)
|
||||
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)>() {
|
||||
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
||||
var handled: Boolean;
|
||||
synchronized(_listeners) {
|
||||
handled = _listeners.isNotEmpty();
|
||||
}
|
||||
var handled = false;
|
||||
|
||||
synchronized(_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) {
|
||||
handled = handled || _listeners.isNotEmpty();
|
||||
for (handler in _listeners)
|
||||
handler.handler.invoke(value1, value2, value3);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class TaskHandler<TParameter, TResult> {
|
||||
fun run(parameter: TParameter) {
|
||||
val id = ++_idGenerator;
|
||||
|
||||
var handled = false;
|
||||
_scope().launch(_dispatcher) {
|
||||
if (id != _idGenerator)
|
||||
return@launch;
|
||||
@@ -67,24 +68,31 @@ class TaskHandler<TParameter, TResult> {
|
||||
return@launch;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (id != _idGenerator)
|
||||
if (id != _idGenerator) {
|
||||
handled = true;
|
||||
return@withContext;
|
||||
}
|
||||
|
||||
try {
|
||||
onSuccess.emit(result);
|
||||
handled = true;
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
|
||||
onError.emit(e, parameter);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
|
||||
if (id != _idGenerator)
|
||||
if (id != _idGenerator) {
|
||||
handled = true;
|
||||
return@launch;
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
handled = true;
|
||||
if (id != _idGenerator)
|
||||
return@withContext;
|
||||
|
||||
@@ -95,7 +103,18 @@ class TaskHandler<TParameter, TResult> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}/*.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
|
||||
|
||||
@@ -416,7 +416,7 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
val resp = _client.get(body.url!!, body.headers);
|
||||
|
||||
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"));
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
package com.futo.platformplayer.downloads
|
||||
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class PlaylistDownloadDescriptor(
|
||||
val id: String,
|
||||
val targetPxCount: 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.SerializedPlatformVideoDetails
|
||||
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.VideoHelper
|
||||
import com.futo.platformplayer.isDownloadable
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -27,7 +31,6 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
@@ -147,27 +150,37 @@ class VideoDownload {
|
||||
if(original !is IPlatformVideoDetails)
|
||||
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());
|
||||
if(videoSource == null && targetPixelCount != null) {
|
||||
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
||||
?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||
else
|
||||
throw IllegalStateException("Download video source is not a url source");
|
||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource != null) {
|
||||
if (vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||
}
|
||||
}
|
||||
|
||||
if(audioSource == null && targetBitrate != null) {
|
||||
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
||||
?: 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)
|
||||
audioSource = null;
|
||||
else if(asource is IAudioUrlSource)
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource);
|
||||
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 {
|
||||
@@ -358,7 +371,7 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
if (isCancelled)
|
||||
throw IllegalStateException("Cancelled");
|
||||
throw CancellationException("Cancelled");
|
||||
} while (read > 0);
|
||||
|
||||
lastSpeed = 0;
|
||||
@@ -410,7 +423,7 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
if(isCancelled)
|
||||
throw IllegalStateException("Cancelled");
|
||||
throw CancellationException("Cancelled", null);
|
||||
}
|
||||
onProgress(sourceLength, totalRead, 0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.futo.platformplayer.engine
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
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.packages.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateAssets
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class V8Plugin {
|
||||
val config: IV8PluginConfig;
|
||||
@@ -31,14 +28,31 @@ class V8Plugin {
|
||||
val httpClient: ManagedHttpClient get() = _client;
|
||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||
|
||||
private val _runtimeLock = Object();
|
||||
var _runtime : V8Runtime? = null;
|
||||
|
||||
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
|
||||
private val _depsPackages : MutableList<V8Package> = mutableListOf();
|
||||
private var _script : String? = null;
|
||||
|
||||
var isStopped = true;
|
||||
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()) {
|
||||
this._client = client;
|
||||
this._clientAuth = clientAuth;
|
||||
@@ -81,7 +95,7 @@ class V8Plugin {
|
||||
|
||||
fun start() {
|
||||
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
||||
synchronized(this) {
|
||||
synchronized(_runtimeLock) {
|
||||
if (_runtime != null)
|
||||
return;
|
||||
|
||||
@@ -121,19 +135,25 @@ class V8Plugin {
|
||||
catchScriptErrors("Plugin[${config.name}]") {
|
||||
it.getExecutor(script).executeVoid()
|
||||
};
|
||||
isStopped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
fun stop(){
|
||||
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
||||
synchronized(this) {
|
||||
_runtime?.let {
|
||||
_runtime = null;
|
||||
if(!it.isClosed && !it.isDead)
|
||||
it.close();
|
||||
};
|
||||
isStopped = true;
|
||||
whenNotBusy {
|
||||
synchronized(_runtimeLock) {
|
||||
isStopped = true;
|
||||
_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 {
|
||||
@@ -141,14 +161,53 @@ class V8Plugin {
|
||||
}
|
||||
fun <T : V8Value> executeTyped(js: String) : T {
|
||||
warnIfMainThread("V8Plugin.executeTyped");
|
||||
if(isStopped)
|
||||
throw PluginEngineStoppedException(config, "Instance is stopped", js);
|
||||
|
||||
synchronized(_busyCounterLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
|
||||
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 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 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 {
|
||||
//TODO: Auto get all package types?
|
||||
return when(packageName) {
|
||||
@@ -160,7 +219,13 @@ class V8Plugin {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -185,7 +250,7 @@ class V8Plugin {
|
||||
if(result is V8ValueObject) {
|
||||
val type = result.getString("plugin_type");
|
||||
if(type != null && type.endsWith("Exception"))
|
||||
Companion.throwExceptionFromV8(
|
||||
throwExceptionFromV8(
|
||||
config,
|
||||
result.getOrThrow(config, "plugin_type", "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);
|
||||
}
|
||||
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(
|
||||
config,
|
||||
executeEx.scriptingError.context["plugin_type"].toString(),
|
||||
(exMessage ?: ""),
|
||||
pluginType,
|
||||
(extractJSExceptionMessage(executeEx) ?: ""),
|
||||
executeEx,
|
||||
executeEx.scriptingError?.stack,
|
||||
codeStripped
|
||||
);
|
||||
|
||||
throw ScriptExecutionException(config, "${exMessage}", null, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
throw ex;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,11 +108,12 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
val obj = runtime.createV8ValueObject();
|
||||
obj.set("url", url);
|
||||
obj.set("code", code);
|
||||
obj.set("body", body);
|
||||
obj.set("headers", headers);
|
||||
@@ -227,7 +228,7 @@ class PackageHttp: V8Package {
|
||||
val resp = client.requestMethod(method, url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
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 responseBody = resp.body?.string();
|
||||
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 responseBody = resp.body?.string();
|
||||
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 responseBody = resp.body?.string();
|
||||
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
|
||||
catch(ex: SocketTimeoutException) {
|
||||
return BridgeHttpResponse(408, null);
|
||||
return BridgeHttpResponse("", 408, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,7 +462,7 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
//Forward timeouts
|
||||
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;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -29,6 +29,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
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.PolycentricProfile
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -76,7 +77,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
}).success {
|
||||
setLoading(false);
|
||||
setPager(it);
|
||||
}.exception<Throwable> {
|
||||
}
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load initial videos.", it);
|
||||
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.constructs.Event1
|
||||
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.PolycentricProfile
|
||||
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));
|
||||
adapter.notifyItemInserted(adapter.childToParentPosition(_authorLinks.size - 1));
|
||||
loadNext();
|
||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
||||
}.exception<ScriptCaptchaRequiredException> { }
|
||||
.exceptionWithParameter<Throwable> { ex, para ->
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||
UIDialogs.toast(requireContext(), "Failed to fetch\n${para}", false)
|
||||
loadNext();
|
||||
|
||||
+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.structures.IPager
|
||||
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.views.FeedStyle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -86,7 +87,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
||||
}
|
||||
})
|
||||
.success { loadedResult(it); }
|
||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
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.structures.IPager
|
||||
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.views.FeedStyle
|
||||
|
||||
@@ -56,6 +57,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
||||
constructor(fragment: CreatorSearchResultsFragment, inflater: LayoutInflater): super(fragment, inflater) {
|
||||
_taskSearch = TaskHandler<String, IPager<PlatformAuthorLink>>({fragment.lifecycleScope}, { query -> StatePlatform.instance.searchChannels(query) })
|
||||
.success { loadedResult(it); }
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
|
||||
+15
-3
@@ -8,21 +8,27 @@ import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.CaptchaActivity
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.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.IPager
|
||||
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.ScriptImplementationException
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.CaptchaWebViewClient
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
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.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
@@ -92,6 +98,7 @@ class HomeFragment : MainFragment() {
|
||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||
})
|
||||
.success { loadedResult(it); }
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<ScriptExecutionException> {
|
||||
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,
|
||||
@@ -100,17 +107,20 @@ class HomeFragment : MainFragment() {
|
||||
);
|
||||
}
|
||||
.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.Action("Ignore", {}),
|
||||
UIDialogs.Action("Sources", { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
.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, {
|
||||
loadResults()
|
||||
});
|
||||
}) {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,6 +141,8 @@ class HomeFragment : MainFragment() {
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
|
||||
+16
-2
@@ -28,6 +28,7 @@ import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -129,6 +130,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!StateSubscriptions.instance.isGlobalUpdating) {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
@@ -168,7 +173,12 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.success { loadedResult(it); }
|
||||
.exception<Throwable> {
|
||||
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() {
|
||||
@@ -251,7 +261,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
} catch (e: Throwable) {
|
||||
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>) {
|
||||
|
||||
+1
-1
@@ -608,7 +608,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_download, "Download", TAG_DOWNLOAD) {
|
||||
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) {
|
||||
|
||||
@@ -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.VideoUnMuxedSourceDescriptor
|
||||
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.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.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -17,6 +20,12 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
|
||||
class VideoHelper {
|
||||
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(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||
val targetVideo = if(desiredPixelCount > 0)
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ class LoginWebViewClient : WebViewClient {
|
||||
onPageLoaded.emit(view, url);
|
||||
}
|
||||
|
||||
//TODO: Use new WebViewRequirementExtractor when time to test extensively
|
||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||
if(request == null)
|
||||
return super.shouldInterceptRequest(view, request as WebResourceRequest?);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.exceptions.DownloadException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.Announcement
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -134,15 +136,21 @@ class DownloadService : Service() {
|
||||
Logger.w(TAG, "Video had no video or videodetail, removing download");
|
||||
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
|
||||
Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex);
|
||||
currentVideo.error = ex.message;
|
||||
currentVideo.changeState(VideoDownload.State.ERROR);
|
||||
ignore.add(currentVideo);
|
||||
|
||||
StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload",
|
||||
"Download failed",
|
||||
"Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download");
|
||||
if(ex !is CancellationException)
|
||||
StateAnnouncement.instance.registerAnnouncement(currentVideo?.id?.value?:"" + currentVideo?.id?.pluginId?:"" + "_FailDownload",
|
||||
"Download failed",
|
||||
"Download for [${currentVideo.name}] failed.\nDownloads are automatically retried.\nReason: ${ex.message}", AnnouncementType.SESSION, null, "download");
|
||||
|
||||
//Give it a sec
|
||||
Thread.sleep(500);
|
||||
|
||||
@@ -25,11 +25,17 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.*
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.CaptchaActivity
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
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.casting.StateCasting
|
||||
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.FileLogConsumer
|
||||
import com.futo.platformplayer.logging.LogLevel
|
||||
@@ -637,6 +643,33 @@ 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;
|
||||
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 {
|
||||
private val TAG = "StateApp";
|
||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.StatFs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
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.exceptions.AlreadyQueuedException
|
||||
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.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
@@ -108,6 +112,11 @@ class StateDownloads {
|
||||
fun getPlaylistDownload(playlistId: String): PlaylistDownloadDescriptor? {
|
||||
return _downloadPlaylists.findItem { it.id == playlistId };
|
||||
}
|
||||
fun savePlaylistDownload(playlistDownload: PlaylistDownloadDescriptor) {
|
||||
synchronized(playlistDownload.preventDownload) {
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
}
|
||||
}
|
||||
fun deleteCachedPlaylist(id: String) {
|
||||
val pdl = getPlaylistDownload(id);
|
||||
if(pdl != null)
|
||||
@@ -142,6 +151,19 @@ class StateDownloads {
|
||||
_downloading.delete(download);
|
||||
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() {
|
||||
val hasPlaylistChanged = checkForOutdatedPlaylists();
|
||||
@@ -157,12 +179,15 @@ class StateDownloads {
|
||||
val playlistsDownloaded = getCachedPlaylists();
|
||||
for(playlist in playlistsDownloaded) {
|
||||
val playlistDownload = getPlaylistDownload(playlist.playlist.id) ?: continue;
|
||||
|
||||
if(playlist.playlist.videos.any{ getCachedVideo(it.id) == null }) {
|
||||
Logger.i(TAG, "Found new videos on playlist [${playlist.playlist.name}]");
|
||||
val toIgnore = playlistDownload.getPreventDownloadList();
|
||||
val missingVideoCount = playlist.playlist.videos.count { !toIgnore.contains(it.url) && getCachedVideo(it.id) == null };
|
||||
if(missingVideoCount > 0) {
|
||||
Logger.i(TAG, "Found new videos (${missingVideoCount}) on playlist [${playlist.playlist.name}] to download");
|
||||
continueDownload(playlistDownload, playlist.playlist);
|
||||
hasChanged = true;
|
||||
}
|
||||
else
|
||||
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||
}
|
||||
return hasChanged;
|
||||
}
|
||||
@@ -171,6 +196,11 @@ class StateDownloads {
|
||||
var hasNew = false;
|
||||
for(item in playlist.videos) {
|
||||
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) {
|
||||
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != 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> {
|
||||
val expected = getDownloadedVideos();
|
||||
val validFiles = HashSet(expected.flatMap { it.videoSource.map { it.filePath } + it.audioSource.map { it.filePath } });
|
||||
|
||||
@@ -172,7 +172,11 @@ class StatePlatform {
|
||||
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
|
||||
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)
|
||||
@@ -287,6 +291,9 @@ class StatePlatform {
|
||||
StatePlugins.instance.getPlugin(id)
|
||||
?: throw IllegalStateException("Client existed, but plugin config didn't")
|
||||
);
|
||||
newClient.onCaptchaException.subscribe { client, ex ->
|
||||
StateApp.instance.handleCaptchaException(client, ex);
|
||||
}
|
||||
|
||||
synchronized(_clientsLock) {
|
||||
if (_enabledClients.contains(client)) {
|
||||
@@ -399,13 +406,15 @@ class StatePlatform {
|
||||
return@async searchResult;
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "getHomeRefresh", ex);
|
||||
return@async null;
|
||||
throw ex;
|
||||
//return@async null;
|
||||
}
|
||||
});
|
||||
}.toList();
|
||||
|
||||
val finishedPager = deferred.map { it.second }.awaitFirstNotNullDeferred() ?: return EmptyPager();
|
||||
val toAwait = deferred.filter { it.second != finishedPager.first };
|
||||
|
||||
return RefreshDistributionContentPager(
|
||||
listOf(finishedPager.second),
|
||||
toAwait.map { it.second },
|
||||
@@ -616,9 +625,13 @@ class StatePlatform {
|
||||
}
|
||||
|
||||
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})");
|
||||
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> {
|
||||
Logger.i(TAG, "Platform - getChannel");
|
||||
@@ -629,9 +642,9 @@ class StatePlatform {
|
||||
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");
|
||||
val baseClient = getChannelClient(channelUrl);
|
||||
val baseClient = getChannelClient(channelUrl, ignorePlugins);
|
||||
val clientCapabilities = baseClient.getChannelCapabilities();
|
||||
|
||||
val client = if(usePooledClients > 1)
|
||||
@@ -657,11 +670,11 @@ class StatePlatform {
|
||||
if(sub != null) {
|
||||
val daysSinceLiveStream = sub.lastLiveStream.getNowDiffDays()
|
||||
if(daysSinceLiveStream > 7) {
|
||||
Logger.i(TAG, "Subscription [${channelUrl}] Last livestream > 7 days, skipping live streams [${daysSinceLiveStream} days ago]");
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${daysSinceLiveStream} days ago]");
|
||||
toQuery.remove(ResultCapabilities.TYPE_LIVE);
|
||||
}
|
||||
if(daysSinceLiveStream > 14) {
|
||||
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 [${daysSinceLiveStream} days ago]");
|
||||
toQuery.remove(ResultCapabilities.TYPE_STREAMS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.futo.platformplayer.R
|
||||
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.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
@@ -265,6 +266,11 @@ class StatePlaylists {
|
||||
builder.messages.add("${name}:[${it}] is no longer available");
|
||||
return@map null;
|
||||
}
|
||||
catch(ex: NoPlatformClientException) {
|
||||
//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) {
|
||||
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.media.platforms.js.JSClient
|
||||
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.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -372,7 +373,7 @@ class StatePlugins {
|
||||
if(icon != null)
|
||||
iconsDir.saveIconBinary(config.id, icon);
|
||||
|
||||
_plugins.save(SourcePluginDescriptor(config, null, flags));
|
||||
_plugins.save(SourcePluginDescriptor(config, null, null, flags));
|
||||
return null;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
@@ -407,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?) {
|
||||
if(id == StateDeveloper.DEV_ID) {
|
||||
StatePlatform.instance.getDevClient()?.let {
|
||||
|
||||
@@ -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
|
||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
||||
@@ -138,7 +138,7 @@ class StatePolycentric {
|
||||
return@mapNotNull null;
|
||||
}
|
||||
|
||||
return@mapNotNull StatePlatform.instance.getChannelContent(url, isSubscriptionOptimized, concurrency);
|
||||
return@mapNotNull StatePlatform.instance.getChannelContent(url, isSubscriptionOptimized, concurrency, ignorePlugins);
|
||||
}.toTypedArray();
|
||||
|
||||
val pager = MultiChronoContentPager(pagers);
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.futo.platformplayer.UIDialogs
|
||||
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.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.ReusablePager.Companion.asReusable
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
@@ -12,6 +14,7 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.findNonRuntimeException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
@@ -230,8 +233,11 @@ class StateSubscriptions {
|
||||
var finished = 0;
|
||||
val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf();
|
||||
val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency();
|
||||
val failedPlugins = arrayListOf<String>();
|
||||
for (sub in getSubscriptions().filter { StatePlatform.instance.hasEnabledChannelClient(it.channel.url) }) {
|
||||
tasks.add(_subscriptionsPool.submit<Pair<Subscription, IPager<IPlatformContent>?>> {
|
||||
val toIgnore = synchronized(failedPlugins){ failedPlugins.toList() };
|
||||
|
||||
var polycentricProfile : PolycentricCache.CachedPolycentricProfile? = null;
|
||||
val getProfileTime = measureTimeMillis {
|
||||
try {
|
||||
@@ -258,9 +264,9 @@ class StateSubscriptions {
|
||||
val time = measureTimeMillis {
|
||||
val profile = polycentricProfile?.profile
|
||||
pager = if (profile != null)
|
||||
StatePolycentric.instance.getChannelContent(profile, true, concurrency)
|
||||
StatePolycentric.instance.getChannelContent(profile, true, concurrency, toIgnore)
|
||||
else
|
||||
StatePlatform.instance.getChannelContent(sub.channel.url, true, concurrency);
|
||||
StatePlatform.instance.getChannelContent(sub.channel.url, true, concurrency, toIgnore);
|
||||
|
||||
if (cacheScope != null)
|
||||
pager = ChannelContentCache.cachePagerResults(cacheScope, pager) {
|
||||
@@ -276,12 +282,22 @@ class StateSubscriptions {
|
||||
);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Subscription [${sub.channel.name}] failed", ex);
|
||||
finished++;
|
||||
onProgress?.invoke(finished, tasks.size);
|
||||
val channelEx = ChannelException(sub.channel, ex);
|
||||
synchronized(exceptionMap) {
|
||||
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, "Subscriptions fetch ignoring plugin [${ex.config.name}] due to Captcha");
|
||||
failedPlugins.add(ex.config.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!withCacheFallback)
|
||||
throw channelEx;
|
||||
else {
|
||||
|
||||
@@ -118,6 +118,7 @@ class ManagedStore<T>{
|
||||
val builder = ReconstructStore.Builder();
|
||||
|
||||
for (recon in items) {
|
||||
onProgress?.invoke(0, total);
|
||||
//Retry once
|
||||
for (i in 0 .. 1) {
|
||||
try {
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.futo.platformplayer.R
|
||||
|
||||
class Loader : LinearLayout {
|
||||
@@ -15,7 +17,7 @@ class Loader : LinearLayout {
|
||||
private val _animatable: Animatable;
|
||||
|
||||
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);
|
||||
_animatable = _imageLoader.drawable as Animatable;
|
||||
|
||||
@@ -29,6 +31,18 @@ class Loader : LinearLayout {
|
||||
|
||||
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() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
+19
-3
@@ -3,8 +3,10 @@ package com.futo.platformplayer.views.adapters
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
@@ -18,6 +20,7 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
private val _loader: ImageView;
|
||||
private val _platformIndicator: PlatformIndicator;
|
||||
private val _error: TextView;
|
||||
|
||||
val context: Context;
|
||||
|
||||
@@ -30,15 +33,28 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
|
||||
context = itemView.context;
|
||||
_loader = itemView.findViewById(R.id.loader);
|
||||
_platformIndicator = itemView.findViewById(R.id.thumbnail_platform);
|
||||
_error = itemView.findViewById(R.id.text_error);
|
||||
|
||||
(_loader.drawable as Animatable?)?.start(); //TODO: stop?
|
||||
(_loader.drawable as Animatable?)?.start();
|
||||
}
|
||||
|
||||
override fun bind(content: IPlatformContent) {
|
||||
if(content is PlatformContentPlaceholder)
|
||||
if(content is PlatformContentPlaceholder) {
|
||||
_platformIndicator.setPlatformFromClientID(content.id.pluginId);
|
||||
else
|
||||
_error.text = content.error?.message ?: "";
|
||||
if(content.error != null) {
|
||||
_loader.visibility = View.GONE;
|
||||
(_loader.drawable as Animatable?)?.stop();
|
||||
}
|
||||
else {
|
||||
_loader.visibility = View.VISIBLE;
|
||||
(_loader.drawable as Animatable?)?.start();
|
||||
}
|
||||
}
|
||||
else {
|
||||
_platformIndicator.clearPlatform();
|
||||
(_loader.drawable as Animatable?)?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
override fun preview(video: IPlatformContentDetails?, paused: Boolean) { }
|
||||
|
||||
@@ -100,7 +100,8 @@ class AnnouncementView : LinearLayout {
|
||||
}
|
||||
|
||||
private fun setAnnouncement(announcement: Announcement?, count: Int) {
|
||||
Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count");
|
||||
if(count == 0 && announcement == null)
|
||||
Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count");
|
||||
|
||||
_currentAnnouncement = announcement;
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ class ActiveDownloadItem: LinearLayout {
|
||||
|
||||
_videoCancel.setOnClickListener {
|
||||
StateDownloads.instance.removeDownload(_download);
|
||||
StateDownloads.instance.preventPlaylistDownload(_download);
|
||||
};
|
||||
|
||||
_download.onProgressChanged.subscribe(this) {
|
||||
|
||||
+7
-1
@@ -40,7 +40,7 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
_groupItems = listOf();
|
||||
}
|
||||
|
||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>): super(context){
|
||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
||||
init(animated, okText);
|
||||
_container = parent;
|
||||
if(!_container!!.children.contains(this)) {
|
||||
@@ -50,6 +50,12 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
_textTitle.text = titleText;
|
||||
_groupItems = items;
|
||||
|
||||
if(hideButtons) {
|
||||
_textCancel.visibility = GONE;
|
||||
_textOK.visibility = GONE;
|
||||
_textTitle.textAlignment = TextView.TEXT_ALIGNMENT_CENTER;
|
||||
}
|
||||
|
||||
setItems(items);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/black">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="Please enter the captcha and close when finished" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="CLOSE" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
@@ -33,6 +33,7 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -49,6 +50,21 @@
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_gravity="center" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_error"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="100dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:textSize="9dp"
|
||||
android:textColor="@color/pastel_red" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -16,6 +16,7 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
@@ -31,7 +32,7 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:paddingTop="27dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="center">
|
||||
|
||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
@@ -43,7 +44,22 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_error"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:textSize="9dp"
|
||||
android:textColor="@color/pastel_red" />
|
||||
</LinearLayout>
|
||||
Submodule app/src/stable/assets/sources/youtube updated: 123960682a...1c34bb0163
Submodule app/src/unstable/assets/sources/youtube updated: 7581696172...1c34bb0163
Reference in New Issue
Block a user