Compare commits

..

1 Commits

Author SHA1 Message Date
Koen 816d3c58f2 POST fix. 2024-01-21 18:37:02 +01:00
113 changed files with 468 additions and 1636 deletions
-6
View File
@@ -58,9 +58,3 @@
[submodule "dep/futopay"]
path = dep/futopay
url = ../futopayclientlibraries.git
[submodule "app/src/unstable/assets/sources/bilibili"]
path = app/src/unstable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git
-9
View File
@@ -402,11 +402,6 @@
<div class="code">
{{req.code}}
</div>
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
<a :href="req.docUrl" target="_blank">
Documentation
</a>
</div>
<div>
<div class="parameter" v-for="parameter in req.parameters">
<div class="name">
@@ -543,7 +538,6 @@
<!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script>
<script src="./dev_bridge.js"></script>
<script>
@@ -580,9 +574,6 @@
Testing: {
requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x;
}),
lastResult: "",
+4 -2
View File
@@ -243,7 +243,9 @@ declare class DashSource implements IVideoSource {
declare interface IRequest {
url: string,
headers: Map<string, string>
headers: Map<string, string>,
method: string,
body?: string
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
@@ -251,7 +253,7 @@ declare interface IRequestModifierDef {
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
modifyRequest(url: string, headers: Map<string, string>, method: string, body?: string): IRequest;
}
//Channel
@@ -1,4 +1,4 @@
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
@@ -13,8 +13,6 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
//Long
@@ -121,8 +119,7 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
}
fun OffsetDateTime.getNowDiffYears(): Long {
val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
return diff.roundToLong();
return ChronoUnit.YEARS.between(this, OffsetDateTime.now());
}
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
@@ -153,7 +150,6 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
if(value >= secondsInYear) {
value = getNowDiffYears();
if(abs) value = abs(value);
value = Math.max(1, value);
unit = "year";
}
else if(value >= secondsInMonth) {
@@ -311,10 +311,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear();
@@ -549,8 +546,6 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.log_levels)
var logLevel: Int = 0;
fun isVerbose() = logLevel >= 4;
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
fun submitLogs() {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
@@ -34,7 +34,6 @@ import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore
@@ -344,8 +343,8 @@ class UIDialogs {
}
}
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -38,17 +37,11 @@ import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup
@@ -94,37 +87,7 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
@@ -510,15 +473,10 @@ class UISlideOverlays {
}
}
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
StateDownloads.instance.download(playlist, px, bitrate);
};
}
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
StateDownloads.instance.downloadWatchLater(px, bitrate);
})
}
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
val items = arrayListOf<View>();
var menu: SlideUpMenuOverlay? = null;
@@ -688,17 +646,9 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
showDownloadVideoOverlay(video, container, true);
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
showDownloadVideoOverlay(video, container, true);
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
@@ -788,8 +738,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
}
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
overlay.show();
return overlay;
}
@@ -41,14 +41,12 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.futo.polycentric.core.ApiMethods
import com.google.gson.JsonParser
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.*
@@ -155,8 +153,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
constructor() : super() {
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
val writer = StringWriter();
@@ -195,7 +191,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.mainAppStarting(this);
@@ -608,7 +603,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.showSingleButtonDialog(
this,
R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
getString(R.string.unknown_content_format) + " [${url}]",
"Ok",
{ });
}
@@ -698,22 +693,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!recon.trim().startsWith("["))
return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
val reconLines = Json.decodeFromString<List<String>>(recon);
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache);
handleReconstruction(recon);
return true;
}
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
@@ -728,25 +711,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) {
var recon = String(readSharedFile(file));
val recon = String(readSharedFile(file));
if(!recon.startsWith("["))
return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache);
handleReconstruction(recon);
return true;
}
else if(file.lowercase().endsWith(".zip")) {
@@ -758,7 +728,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
return false;
}
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
fun handleReconstruction(recon: String) {
val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) {
"Playlist" -> StatePlaylists.instance.playlistStore
@@ -775,7 +745,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
UIDialogs.showImportDialog(this, store, name, listOf(recon)) {
}
}
@@ -12,7 +12,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
@@ -71,12 +70,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
@@ -127,12 +126,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
@@ -60,9 +60,6 @@ class CachedPlatformClient : IPlatformClient {
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
@@ -84,15 +84,6 @@ interface IPlatformClient {
*/
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
/**
* Describes what the plugin is capable on peek channel results
*/
fun getPeekChannelTypes(): List<String>;
/**
* Peeks contents of a channel, upload time descending
*/
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
/**
* Gets the channel url associated with a claimType
*/
@@ -13,12 +13,10 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false
val hasGetContentChapters: Boolean = false
) {
}
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
val id: PlatformID;
val name: String;
val url: String;
var thumbnail: String?;
val thumbnail: String?;
var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@@ -3,8 +3,6 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@kotlinx.serialization.Serializable
@@ -33,7 +31,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
.map { Thumbnail.fromV8(it as V8ValueObject) }
.toTypedArray());
}
}
@@ -42,10 +40,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
fun fromV8(value: V8ValueObject): Thumbnail {
return Thumbnail(
value.getOrDefault<String>(config,"url", "Thumbnail", null),
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
value.getString("url"),
value.getInteger("quality"));
}
}
};
@@ -37,10 +37,6 @@ class SerializedChannel(
TODO("Not yet implemented")
}
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel(
@@ -1,14 +1,14 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
val _handler: (String, Map<String,String>, String, String?)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
constructor(modifyReq: (String, Map<String,String>, String, String?)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
override fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest {
return _handler(url, headers, method, body);
}
}
@@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val method: String?;
val url: String?;
val headers: Map<String, String>;
val headers: Map<String, String>?;
val body: String?;
}
@@ -3,5 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest
}
@@ -27,7 +27,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
@@ -59,7 +58,6 @@ import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction
import kotlin.streams.asSequence
open class JSClient : IPlatformClient {
val config: SourcePluginConfig;
@@ -75,7 +73,6 @@ open class JSClient : IPlatformClient {
private var _searchCapabilities: ResultCapabilities? = null;
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null;
protected val _script: String;
@@ -94,11 +91,7 @@ open class JSClient : IPlatformClient {
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0;
val isBusyAction: String get() {
return _busyAction;
}
val settings: HashMap<String, String?> get() = descriptor.settings;
@@ -157,8 +150,6 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it);
};
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
this._context = context;
@@ -182,8 +173,6 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it);
};
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
open fun getCopy(): JSClient {
@@ -225,11 +214,9 @@ open class JSClient : IPlatformClient {
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
);
try {
@@ -273,7 +260,7 @@ open class JSClient : IPlatformClient {
}
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getHome()"));
@@ -281,7 +268,7 @@ open class JSClient : IPlatformClient {
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
ensureEnabled();
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
.toArray()
@@ -311,7 +298,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
@@ -319,9 +306,6 @@ open class JSClient : IPlatformClient {
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
if(!capabilities.hasGetSearchChannelContentsCapabilities)
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
ensureEnabled();
if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!;
@@ -335,7 +319,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
if(!capabilities.hasSearchChannelContents)
throw IllegalStateException("This plugin does not support channel search");
@@ -347,7 +331,7 @@ open class JSClient : IPlatformClient {
@JSOptional
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
@JSDocsParameter("query", "Query that channels should match")
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
ensureEnabled();
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
@@ -367,7 +351,7 @@ open class JSClient : IPlatformClient {
}
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
ensureEnabled();
return@isBusyWith JSChannel(config,
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
@@ -394,46 +378,12 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
}
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
override fun getPeekChannelTypes(): List<String> {
if(!capabilities.hasPeekChannelContents)
return listOf();
try {
if (_peekChannelTypes != null) {
return _peekChannelTypes!!;
}
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
_peekChannelTypes = arr.keys.mapNotNull {
val str = arr.get<V8ValueString>(it);
return@mapNotNull str.value;
};
return _peekChannelTypes ?: listOf();
}
catch(ex: Throwable) {
announcePluginUnhandledException("getPeekChannelTypes", ex);
return listOf();
}
}
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
ensureEnabled();
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
return@isBusyWith items.keys.mapNotNull {
val obj = items.get<V8ValueObject>(it);
return@mapNotNull IJSContent.fromV8(this, obj);
};
}
@JSOptional
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
@JSDocsParameter("claimType", "Polycentric claimtype id")
@@ -494,7 +444,7 @@ open class JSClient : IPlatformClient {
}
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@JSDocsParameter("url", "A content url (this platform)")
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
ensureEnabled();
return@isBusyWith IJSContentDetails.fromV8(this,
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
@@ -503,7 +453,7 @@ open class JSClient : IPlatformClient {
@JSOptional //getContentChapters = function(url, initialData)
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
@JSDocsParameter("url", "A content url (this platform)")
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
if(!capabilities.hasGetContentChapters)
return@isBusyWith listOf();
ensureEnabled();
@@ -514,7 +464,7 @@ open class JSClient : IPlatformClient {
@JSOptional
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
@JSDocsParameter("url", "A content url (this platform)")
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
if(!capabilities.hasGetPlaybackTracker)
return@isBusyWith null;
ensureEnabled();
@@ -528,7 +478,7 @@ open class JSClient : IPlatformClient {
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
@JSDocsParameter("url", "A content url (this platform)")
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
ensureEnabled();
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
@@ -546,7 +496,7 @@ open class JSClient : IPlatformClient {
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream")
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
if(!capabilities.hasGetLiveChatWindow)
return@isBusyWith null;
ensureEnabled();
@@ -555,7 +505,7 @@ open class JSClient : IPlatformClient {
}
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream")
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
if(!capabilities.hasGetLiveEvents)
return@isBusyWith null;
ensureEnabled();
@@ -568,7 +518,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
@JSDocsParameter("channelId", "(optional) Channel id to search in")
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
if(!capabilities.hasSearchPlaylists)
throw IllegalStateException("This plugin does not support playlist search");
@@ -578,22 +528,15 @@ open class JSClient : IPlatformClient {
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean {
ensureEnabled();
if (!capabilities.hasGetPlaylist)
return false;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
}
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
}
@JSOptional
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
@JSDocsParameter("url", "Url of playlist")
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
ensureEnabled();
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
}
@@ -690,24 +633,19 @@ open class JSClient : IPlatformClient {
}
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
private fun <T> isBusyWith(handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
}
_busyAction = actionName;
return handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
}
}
}
private fun <T> isBusyWith(handle: ()->T): T {
return isBusyWith("Unknown", handle);
}
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
if(ex is PluginEngineException)
@@ -724,43 +662,10 @@ open class JSClient : IPlatformClient {
companion object {
val TAG = "JSClient";
private val _lock = Object();
private var _docs: Map<String, String>? = null;
fun getMethodDocs(names: List<String>): Map<String, String>? {
synchronized(_lock) {
if(_docs == null) {
val client = ManagedHttpClient();
val docs = names
.map { stringWithoutBrackets(it) }
.distinct()
.parallelStream()
.map {
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
val resp = client.head(url);
if(resp.isOk)
return@map Pair(it, url);
else
return@map null;
}.asSequence()
.filterNotNull()
.toMap();
_docs = docs;
}
return _docs;
}
}
fun getMethodDocUrls(): Map<String, String>? {
if(_docs != null)
return _docs;
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
return getMethodDocs(methods.map { it.name });
}
fun getJSDocs(): List<JSCallDocs> {
val docs = mutableListOf<JSCallDocs>();
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
val doc = method.getAnnotation(JSDocs::class.java);
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
@@ -773,12 +678,5 @@ open class JSClient : IPlatformClient {
}
return docs;
}
private fun stringWithoutBrackets(name: String): String {
val index = name.indexOf('(');
if(index >= 0)
return name.substring(0, index);
return name;
}
}
}
@@ -45,8 +45,7 @@ class SourcePluginConfig(
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null
var primaryClaimFieldType: Int? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -8,7 +8,6 @@ import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.serialization.Serializable
@Serializable
@@ -91,7 +90,7 @@ class SourcePluginDescriptor {
@Serializable
class AppPluginSettings {
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
var checkForUpdates: Boolean = true;
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
@@ -131,11 +130,6 @@ class SourcePluginDescriptor {
}
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
var allowDeveloperSubmit: Boolean = false;
fun loadDefaults(config: SourcePluginConfig) {
if(tabEnabled.enableHome == null)
tabEnabled.enableHome = config.enableInHome
@@ -14,6 +14,6 @@ annotation class JSOptional()
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
@kotlinx.serialization.Serializable
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
@kotlinx.serialization.Serializable
data class JSParameterDocs(val name: String, val description: String);
@@ -12,31 +12,40 @@ import com.futo.platformplayer.getOrDefault
class JSRequest : IRequest {
private val _v8Url: String?;
private val _v8Headers: Map<String, String>?;
private val _v8Method: String?;
private val _v8Body: String?;
private val _v8Options: Options?;
override var url: String? = null;
override lateinit var headers: Map<String, String>;
override var method: String? = null;
override var body: String? = null;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, method: String?, body: String?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?) {
_v8Url = url;
_v8Headers = headers;
_v8Method = method;
_v8Body = body;
_v8Options = options;
initialize(plugin, originalUrl, originalHeaders);
initialize(plugin, originalUrl, originalHeaders, originalMethod, originalBody);
}
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?, applyOtherHeadersByDefault: Boolean = false) {
val contextName = "ModifyRequestResponse";
val config = plugin.config;
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
_v8Method = obj.getOrDefault<String>(config, "method", contextName, null);
_v8Body = obj.getOrDefault<String>(config, "body", contextName, null);
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it, applyOtherHeadersByDefault);
} ?: Options(null, null, applyOtherHeadersByDefault);
initialize(plugin, originalUrl, originalHeaders);
initialize(plugin, originalUrl, originalHeaders, originalMethod, originalBody);
}
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config;
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?) {
url = _v8Url ?: originalUrl;
method = _v8Method ?: originalMethod;
body = _v8Body ?: originalBody;
if(_v8Options?.applyOtherHeaders ?: false) {
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
@@ -69,8 +78,8 @@ class JSRequest : IRequest {
}
}
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Method, _v8Body, _v8Options, originalUrl, originalHeaders, originalMethod, originalBody);
}
@@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
@@ -8,9 +7,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
@@ -30,20 +27,20 @@ class JSRequestModifier: IRequestModifier {
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
override fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest {
if (_modifier.isClosed) {
return Request(url, headers);
return Request(url, headers, method, body);
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
_modifier.invoke("modifyRequest", url, headers, method, body);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
val req = JSRequest(_plugin, result, url, headers, method, body);
result.close();
return req;
}
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
data class Request(override val url: String, override val headers: Map<String, String>, override val method: String, override val body: String?) : IRequest;
}
@@ -1,7 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
@@ -15,7 +13,6 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
abstract class JSSource {
protected val _plugin: JSClient;
@@ -33,15 +30,15 @@ abstract class JSSource {
this.type = type;
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null, true);
JSRequest(plugin, it, null, null, null, null, true);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
}
fun getRequestModifier(): IRequestModifier? {
if(_requestModifier != null)
return AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
return AdhocRequestModifier { url, headers, method, body ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers, method, body);
};
if (!hasRequestModifier || _obj.isClosed) {
@@ -163,25 +163,24 @@ class AirPlayCastingDevice : CastingDevice {
}
connectionState = CastConnectionState.CONNECTED;
delay(1000);
val progressIndex = progressInfo.lowercase().indexOf("position: ");
if (progressIndex == -1) {
delay(1000);
continue;
}
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
setTime(progress);
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
if (durationIndex == -1) {
delay(1000);
continue;
}
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
setDuration(duration);
delay(1000);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
}
@@ -44,9 +44,7 @@ class ChromecastCastingDevice : CastingDevice {
private var _socket: SSLSocket? = null;
private var _outputStream: DataOutputStream? = null;
private var _outputStreamLock = Object();
private var _inputStream: DataInputStream? = null;
private var _inputStreamLock = Object();
private var _scopeIO: CoroutineScope? = null;
private var _requestId = 1;
private var _started: Boolean = false;
@@ -385,44 +383,39 @@ class ChromecastCastingDevice : CastingDevice {
getStatus();
val buffer = ByteArray(409600);
val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving.");
while (_scopeIO?.isActive == true) {
try {
val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte();
val b2 = inputStream.readUnsignedByte();
val b3 = inputStream.readUnsignedByte();
val b4 = inputStream.readUnsignedByte();
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong());
continue;
}
synchronized(_inputStreamLock)
{
Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte();
val b2 = inputStream.readUnsignedByte();
val b3 = inputStream.readUnsignedByte();
val b4 = inputStream.readUnsignedByte();
val size =
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong());
return@synchronized
}
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size);
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size);
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
Logger.i(TAG, "Received message: $message");
}
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
Logger.i(TAG, "Received message: $message");
}
try {
handleMessage(message);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e);
}
try {
handleMessage(message);
} catch (e:Throwable) {
Logger.w(TAG, "Failed to handle message.", e);
}
} catch (e: java.net.SocketException) {
Logger.e(TAG, "Socket exception while receiving.", e);
@@ -595,16 +588,13 @@ class ChromecastCastingDevice : CastingDevice {
return;
}
synchronized(_outputStreamLock)
{
val serializedSizeBE = ByteArray(4);
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
serializedSizeBE[3] = (data.size and 0xff).toByte();
outputStream.write(serializedSizeBE);
outputStream.write(data);
}
val serializedSizeBE = ByteArray(4);
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
serializedSizeBE[3] = (data.size and 0xff).toByte();
outputStream.write(serializedSizeBE);
outputStream.write(data);
//Log.d(TAG, "Sent ${data.size} bytes.");
}
@@ -242,7 +242,6 @@ class StateCasting {
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
if (BuildConfig.DEBUG) {
jmDNS.addServiceTypeListener(_serviceTypeListener);
@@ -104,17 +104,6 @@ class DeveloperEndpoints(private val context: Context) {
@HttpGET("/source_docs.js", "application/javascript")
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
@HttpGET("/source_doc_urls.json", "application/json")
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
val docs = JSClient.getMethodDocUrls();
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
}
@HttpGET("/source_doc_urls.js", "application/javascript")
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
val docs = JSClient.getMethodDocUrls();
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
}
//Dependencies
//@HttpGET("/dependencies/vue.js", "application/javascript")
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
@@ -22,9 +22,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -68,15 +66,13 @@ class ImportDialog : AlertDialog {
private val _name: String;
private val _toImport: List<String>;
private val _cache: ImportCache?;
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) {
_context = context;
_store = importStore;
_onConcluded = onConcluded;
_name = name;
_toImport = ArrayList(toReconstruct);
_cache = cache;
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -150,7 +146,7 @@ class ImportDialog : AlertDialog {
val scope = StateApp.instance.scopeOrNull;
scope?.launch(Dispatchers.IO) {
try {
val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
val migrationResult = _store.importReconstructions(_toImport) { finished, total ->
scope.launch(Dispatchers.Main) {
_textProgress.text = "${finished}/${total}";
}
@@ -755,7 +755,6 @@ class VideoDownload {
companion object {
const val TAG = "VideoDownload";
const val GROUP_PLAYLIST = "Playlist";
const val GROUP_WATCHLATER= "WatchLater";
fun videoContainerToExtension(container: String): String? {
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
@@ -7,7 +7,6 @@ import androidx.documentfile.provider.DocumentFile
import com.arthenica.ffmpegkit.*
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanBitrate
@@ -64,7 +63,7 @@ class VideoExport {
val outputFile: DocumentFile?;
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
if (sourceCount > 1) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile("video/mp4", outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -80,7 +79,7 @@ class VideoExport {
}
outputFile = f;
} else if (v != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile(v.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -92,7 +91,7 @@ class VideoExport {
outputFile = f;
} else if (a != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
val f = downloadRoot.createFile(a.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -111,6 +110,11 @@ class VideoExport {
return@coroutineScope outputFile;
}
private fun toSafeFileName(input: String): String {
val safeCharacters = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_')
return input.map { if (it in safeCharacters) it else '_' }.joinToString(separator = "")
}
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
suspendCancellableCoroutine { continuation ->
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
@@ -11,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
@@ -44,6 +45,7 @@ class V8Plugin {
private val _clientAuth: ManagedHttpClient;
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
@@ -69,11 +71,6 @@ class V8Plugin {
private var _busyCounter = 0;
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
var allowDevSubmit: Boolean = false
private set(value) {
field = value;
}
/**
* Called before a busy counter is about to be removed.
* Is primarily used to prevent additional calls to dead runtimes.
@@ -95,10 +92,6 @@ class V8Plugin {
withDependency(getPackage(pack));
}
fun changeAllowDevSubmit(isAllowed: Boolean) {
allowDevSubmit = isAllowed;
}
fun withDependency(context: Context, assetPath: String) : V8Plugin {
if(!_deps.containsKey(assetPath))
_deps.put(assetPath, getAssetFile(context, assetPath));
@@ -5,7 +5,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
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.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.engine.IV8PluginConfig
@@ -13,9 +12,6 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class PackageBridge : V8Package {
@Transient
@@ -25,7 +21,6 @@ class PackageBridge : V8Package {
@Transient
private val _clientAuth: ManagedHttpClient
override val name: String get() = "Bridge";
override val variableName: String get() = "bridge";
@@ -52,44 +47,6 @@ class PackageBridge : V8Package {
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
}
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
private var _devSubmitClient: ManagedHttpClient? = null;
@V8Function
fun devSubmit(label: String, data: String) {
if(_plugin.config !is SourcePluginConfig)
return;
if(!_plugin.allowDevSubmit)
return;
val devUrl = _plugin.config.developerSubmitUrl ?: return;
if(_devSubmitClient == null)
_devSubmitClient = ManagedHttpClient();
val stackTrace = Thread.currentThread().stackTrace;
val callerMethod = stackTrace.findLast {
it.className == JSClient::class.java.name
}?.methodName ?: "";
val session = StateApp.instance.sessionId;
val pluginId = _plugin.config.id;
val pluginVersion = _plugin.config.version;
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val json = _jsonSerializer.encodeToString(obj);
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
}
catch(ex: Exception) {
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
}
}
}
@Serializable
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
@V8Function
fun throwTest(str: String) {
throw IllegalStateException(str);
@@ -4,11 +4,8 @@ import android.util.Base64
import com.caoccao.javet.annotations.V8Function
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.google.common.hash.Hashing.md5
import java.security.MessageDigest
import java.util.UUID
class PackageUtilities : V8Package {
@Transient
private val _config: IV8PluginConfig;
@@ -22,31 +19,7 @@ class PackageUtilities : V8Package {
@V8Function
fun toBase64(arr: ByteArray): String {
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
}
@V8Function
fun fromBase64(str: String): ByteArray {
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
}
@V8Function
fun md5(arr: ByteArray): ByteArray {
return MessageDigest.getInstance("MD5").digest(arr);
}
@V8Function
fun md5String(str: String): String {
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
}
@V8Function
fun sha256(arr: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(arr);
}
@V8Function
fun sha256String(str: String): String {
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
return Base64.encodeToString(arr, Base64.NO_WRAP);
}
@V8Function
@@ -60,10 +60,8 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>();
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
Logger.i(TAG, "getContentPager");
@@ -105,9 +103,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
}).success {
setLoading(false);
val posBefore = _results.size;
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(it);
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.size); };
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(toAdd);
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
@@ -159,7 +157,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
}
@@ -26,7 +26,6 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
@@ -207,12 +206,6 @@ class ChannelFragment : MainFragment() {
StatePlayer.instance.addToQueue(content);
}
}
adapter.onAddToWatchLaterClicked.subscribe { content ->
if(content is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
UIDialogs.toast("Added to watch later\n[${content.name}]");
}
}
adapter.onUrlClicked.subscribe { url ->
fragment.navigate<BrowserFragment>(url);
}
@@ -271,7 +264,7 @@ class ChannelFragment : MainFragment() {
_taskLoadPolycentricProfile.cancel();
_selectedTabIndex = -1;
if (!isBack || _url == null) {
if (!isBack) {
_imageBanner.setImageDrawable(null);
if (parameter is String) {
@@ -1,9 +1,7 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Browser
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -120,7 +118,6 @@ class CommentsFragment : MainFragment() {
holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick);
holder.onAuthorClick.subscribe(::onAuthorClick);
return@InsertedViewAdapterWithLoader holder;
}
);
@@ -214,19 +211,6 @@ class CommentsFragment : MainFragment() {
setRepliesOverlayVisible(true, true)
}
}
private fun onAuthorClick(c: IPlatformComment) {
if (c !is PolycentricPlatformComment) {
return@onAuthorClick;
}
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
//_fragment.navigate<BrowserFragment>(navUrl);
}
}
private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0;
@@ -12,12 +12,10 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
@@ -83,12 +81,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.addToQueue(it);
}
};
adapter.onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
}
};
adapter.onLongPress.subscribe(this) {
if (it is IPlatformVideo) {
showVideoOptionsOverlay(it)
@@ -143,7 +135,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onChannelClicked.remove(this);
adapter.onAddToClicked.remove(this);
adapter.onAddToQueueClicked.remove(this);
adapter.onAddToWatchLaterClicked.remove(this);
adapter.onLongPress.remove(this);
}
@@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
onFilterClick.subscribe(this) {
_overlayContainer.let {
val filterValuesCopy = HashMap(_filterValues);
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
if (changed) {
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
@@ -170,11 +170,7 @@ class ContentSearchResultsFragment : MainFragment() {
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val commonCapabilities =
if(_channelUrl == null)
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
else
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
val sorts = commonCapabilities?.sorts ?: listOf();
if (sorts.size > 1) {
withContext(Dispatchers.Main) {
@@ -12,10 +12,8 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.others.ProgressBar
@@ -145,7 +143,6 @@ class DownloadsFragment : MainFragment() {
val activeDownloads = StateDownloads.instance.getDownloading();
val playlists = StateDownloads.instance.getCachedPlaylists();
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
val downloaded = StateDownloads.instance.getDownloadedVideos()
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
@@ -153,35 +150,23 @@ class DownloadsFragment : MainFragment() {
_listActiveDownloadsContainer.visibility = GONE;
else {
_listActiveDownloadsContainer.visibility = VISIBLE;
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
_listActiveDownloads.removeAllViews();
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
_listActiveDownloads.addView(view);
}
if(playlists.isEmpty() && watchLaterDownload == null)
if(playlists.isEmpty())
_listPlaylistsContainer.visibility = GONE;
else {
_listPlaylistsContainer.visibility = VISIBLE;
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
_listPlaylists.removeAllViews();
if(watchLaterDownload != null) {
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
pdView.setOnClickListener {
_frag.navigate<WatchLaterFragment>();
}
_listPlaylists.addView(pdView);
}
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
view.setOnClickListener {
if(view.obj is Playlist) {
_frag.navigate<PlaylistFragment>(view.obj);
}
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
};
_listPlaylists.addView(view);
}
@@ -201,18 +201,14 @@ class PlaylistFragment : MainFragment() {
showConvertPlaylistButton();
}
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
}
updateDownloadState();
}
fun onResume() {
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
updateDownloadState();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
@@ -221,9 +217,7 @@ class PlaylistFragment : MainFragment() {
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
updateDownloadState();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
@@ -231,12 +225,6 @@ class PlaylistFragment : MainFragment() {
};
}
private fun download() {
_playlist?.let {
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
}
}
fun onPause() {
StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this);
@@ -280,6 +268,43 @@ class PlaylistFragment : MainFragment() {
}
}
private fun updateDownloadState() {
val playlist = _playlist ?: return;
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
if(isDownloaded && !isDownloading)
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
else
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
if(isDownloading) {
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
});
}
}
else if(isDownloaded) {
_buttonDownload.setImageResource(R.drawable.ic_download_off);
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
});
}
}
else {
_buttonDownload.setImageResource(R.drawable.ic_download);
_buttonDownload.setOnClickListener {
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
}
}
_buttonDownload.setPadding(dp10.toInt());
}
override fun canEdit(): Boolean { return _playlist != null; }
@@ -35,7 +35,6 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric
@@ -212,8 +211,6 @@ class PostDetailFragment : MainFragment {
_repliesOverlay = findViewById(R.id.replies_overlay);
_textContent.setPlatformPlayerLinkMovementMethod(context);
_buttonSubscribe.onSubscribed.subscribe {
//TODO: add overlay to layout
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
@@ -476,7 +473,6 @@ class PostDetailFragment : MainFragment {
}
updateCommentType(true);
setLoading(false);
}
fun setPostOverview(value: IPlatformPost) {
@@ -12,7 +12,6 @@ import android.webkit.CookieManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@@ -108,20 +107,17 @@ class SourceDetailFragment : MainFragment() {
fun onHide() {
val id = _config?.id ?: return;
var shouldReload = false;
if(_settingsAppChanged) {
_settingsAppForm.setObjectValues();
StatePlugins.instance.savePlugin(id);
shouldReload = true;
}
if(_settingsChanged && _settings != null) {
_settingsChanged = false;
StatePlugins.instance.setPluginSettings(id, _settings!!);
shouldReload = true;
reloadSource(id);
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
}
if(shouldReload)
reloadSource(id);
if(_settingsAppChanged) {
_settingsAppForm.setObjectValues();
StatePlugins.instance.savePlugin(id);
}
}
@@ -141,25 +137,9 @@ class SourceDetailFragment : MainFragment() {
//App settings
try {
_settingsAppForm.fromObject(source.descriptor.appSettings);
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
val field = _settingsAppForm.findField("devSubmit");
field?.setValue(false);
if(field is View)
field.isVisible = false;
}
_settingsAppForm.onChanged.clear();
_settingsAppForm.onChanged.subscribe { field, value ->
_settingsAppForm.onChanged.subscribe { _, _ ->
_settingsAppChanged = true;
if(field.descriptor?.id == "devSubmit") {
if(value is Boolean && value) {
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
"Are you sure you trust the developer?",
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
source.config.developerSubmitUrl ?: "", 0,
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
}
}
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
@@ -25,7 +25,6 @@ import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
@@ -198,7 +197,6 @@ class SubscriptionsFeedFragment : MainFragment() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
var allowLive: Boolean = true;
var allowPlanned: Boolean = false;
var allowWatched: Boolean = true;
override fun encode(): String {
return Json.encodeToString(this);
}
@@ -306,8 +304,7 @@ class SubscriptionsFeedFragment : MainFragment() {
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
);
}
@@ -339,9 +336,6 @@ class SubscriptionsFeedFragment : MainFragment() {
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
return@filter false;
//TODO: Check against a sub cache
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
return@filter false;
@@ -23,7 +23,6 @@ import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.webkit.WebView
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
@@ -125,7 +124,6 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.overlays.SupportOverlay
import com.futo.platformplayer.views.overlays.WebviewOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
@@ -246,7 +244,6 @@ class VideoDetailView : ConstraintLayout {
private val _container_content_replies: RepliesOverlay;
private val _container_content_description: DescriptionOverlay;
private val _container_content_liveChat: LiveChatOverlay;
private val _container_content_browser: WebviewOverlay;
private val _container_content_support: SupportOverlay;
private var _container_content_current: View;
@@ -352,8 +349,7 @@ class VideoDetailView : ConstraintLayout {
_container_content_replies = findViewById(R.id.videodetail_container_replies);
_container_content_description = findViewById(R.id.videodetail_container_description);
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
_container_content_support = findViewById(R.id.videodetail_container_support);
_container_content_browser = findViewById(R.id.videodetail_container_webview)
_container_content_support = findViewById(R.id.videodetail_container_support)
_textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view);
@@ -402,10 +398,6 @@ class VideoDetailView : ConstraintLayout {
}
}
};
_monetization.onUrlTap.subscribe {
fragment.navigate<BrowserFragment>(it);
onMinimize.emit();
}
_player.attachPlayer();
@@ -628,7 +620,6 @@ class VideoDetailView : ConstraintLayout {
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
_description_viewMore.setOnClickListener {
switchContentView(_container_content_description);
@@ -649,20 +640,6 @@ class VideoDetailView : ConstraintLayout {
_container_content_current = _container_content_main;
_commentsList.onAuthorClick.subscribe { c ->
if (c !is PolycentricPlatformComment) {
return@subscribe;
}
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
//_container_content_browser.goto(navUrl);
//switchContentView(_container_content_browser);
}
};
_commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0;
var metadata = "";
@@ -1058,10 +1035,10 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main);
}
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0, bypassSameVideoCheck: Boolean = false) {
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) {
Logger.i(TAG, "setVideoOverview")
if(!bypassSameVideoCheck && this.video?.url == video.url)
if(this.video?.url == video.url)
return;
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
@@ -1686,7 +1663,7 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next, true, 0, true);
setVideoOverview(next);
}
}
@@ -1696,7 +1673,7 @@ class VideoDetailView : ConstraintLayout {
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
if(next != null) {
setVideoOverview(next, true, 0, true);
setVideoOverview(next);
return true;
}
else
@@ -2231,11 +2208,11 @@ class VideoDetailView : ConstraintLayout {
videoSourceHeight = 9;
}
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
if(aspectRatio > 2.38) {
if(aspectRatio > 3) {
videoSourceWidth = 16;
videoSourceHeight = 9;
}
else if(aspectRatio < 0.43) {
else if(aspectRatio < 0.3) {
videoSourceHeight = 16;
videoSourceWidth = 9;
}
@@ -2585,7 +2562,7 @@ class VideoDetailView : ConstraintLayout {
}
else
withContext(Dispatchers.Main) {
setVideoDetails(videoDetail, false);
setVideoDetails(videoDetail, true);
_liveTryJob = null;
}
}
@@ -1,7 +1,6 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.graphics.drawable.Animatable
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
@@ -9,17 +8,10 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.setPadding
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.lists.VideoListEditorView
abstract class VideoListEditorView : LinearLayout {
@@ -93,44 +85,6 @@ abstract class VideoListEditorView : LinearLayout {
}
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
//val playlist = _playlist ?: return;
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
if(isDownloaded && !isDownloading)
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
else
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
if(isDownloading) {
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlistId);
});
}
}
else if(isDownloaded) {
_buttonDownload.setImageResource(R.drawable.ic_download_off);
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlistId);
});
}
}
else {
_buttonDownload.setImageResource(R.drawable.ic_download);
_buttonDownload.setOnClickListener {
onDownload();
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
}
}
_buttonDownload.setPadding(dp10.toInt());
}
protected fun setName(name: String?) {
_textName.text = name ?: "";
@@ -5,17 +5,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class WatchLaterFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -35,11 +28,6 @@ class WatchLaterFragment : MainFragment() {
return view;
}
override fun onResume() {
super.onResume()
_view?.onResume();
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
@@ -57,34 +45,6 @@ class WatchLaterFragment : MainFragment() {
fun onShown() {
setName("Watch Later");
setVideos(StatePlaylists.instance.getWatchLater(), true);
setButtonDownloadVisible(true);
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
}
fun onResume(){
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
}
};
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
}
};
}
fun download(){
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
}
override fun onPlayAllClick() {
@@ -116,7 +76,6 @@ class WatchLaterFragment : MainFragment() {
}
companion object {
val TAG = "WatchLaterFragment";
fun newInstance() = WatchLaterFragment().apply {}
}
}
@@ -2,17 +2,11 @@ package com.futo.platformplayer.helpers
class FileHelper {
companion object {
fun String.sanitizeFileName(allowSpace: Boolean = false): String {
return this.filter {
(it in '0' .. '9') ||
(it in 'a'..'z') ||
(it in 'A'..'Z') ||
(it == '-' || it == '.' || it == '_' || (it == ' ' && allowSpace)) ||
(it in '丁'..'龤') || //Chinese/Kanji
(it in '\u3040'..'\u309f') || //Hiragana
(it in '\u30A0'..'\u30ff') || //Katakana
(it in '\u0600'..'\u06FF') //Arabic
}; //Chinese
val allowedCharacters = HashSet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-.".toCharArray().toList());
fun String.sanitizeFileName(): String {
return this.filter { allowedCharacters.contains(it) };
}
}
}
@@ -43,8 +43,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
}
while (_linesToWrite.isNotEmpty()) {
val todo = _linesToWrite.remove()
_writer?.appendLine(todo);
_writer?.appendLine(_linesToWrite.remove());
}
_writer?.flush();
@@ -86,7 +85,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
_running = false;
_writer?.close();
_writer = null;
//_logThread?.join();
_logThread?.join();
_logThread = null;
}
@@ -30,7 +30,7 @@ class HistoryVideo {
}
companion object {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo {
var index = str.indexOf("|||");
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
val url = str.substring(0, index);
@@ -1,11 +0,0 @@
package com.futo.platformplayer.models
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import kotlinx.serialization.Serializable
@Serializable
class ImportCache(
var videos: List<SerializedPlatformVideo>? = null,
var channels: List<SerializedChannel>? = null
);
@@ -40,9 +40,6 @@ class Subscription {
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
//Last video interval
var uploadInterval : Int = 0;
var uploadStreamInterval : Int = 0;
@@ -129,7 +126,6 @@ class Subscription {
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
lastPeekVideo = OffsetDateTime.MIN;
}
ResultCapabilities.TYPE_MIXED -> {
uploadInterval = interval;
@@ -138,7 +134,6 @@ class Subscription {
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
lastPeekVideo = OffsetDateTime.MIN;
}
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
uploadInterval = interval;
@@ -1,42 +0,0 @@
package com.futo.platformplayer.polycentric
import com.futo.platformplayer.encryption.GEncryptionProviderV1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.polycentric.core.ProcessSecret
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.toBase64
import userpackage.Protocol
class PolycentricStorage {
private val _processSecrets = FragmentedStorage.get<StringArrayStorage>("processSecrets");
fun addProcessSecret(processSecret: ProcessSecret) {
_processSecrets.addDistinct(GEncryptionProviderV1.instance.encrypt(processSecret.toProto().toByteArray()).toBase64())
_processSecrets.saveBlocking()
}
fun getProcessSecrets(): List<ProcessSecret> {
val processSecrets = arrayListOf<ProcessSecret>()
for (p in _processSecrets.getAllValues()) {
try {
processSecrets.add(ProcessSecret.fromProto(Protocol.StorageTypeProcessSecret.parseFrom(GEncryptionProviderV1.instance.decrypt(p.base64ToByteArray()))))
} catch (e: Throwable) {
Logger.i(TAG, "Failed to decrypt process secret", e);
}
}
return processSecrets
}
companion object {
val TAG = "PolycentricStorage";
private var _instance : PolycentricStorage? = null;
val instance : PolycentricStorage
get(){
if(_instance == null)
_instance = PolycentricStorage();
return _instance!!;
};
}
}
@@ -37,7 +37,6 @@ class DownloadService : Service() {
private val DOWNLOAD_NOTIF_ID = 3;
private val DOWNLOAD_NOTIF_TAG = "download";
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
//Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
@@ -96,7 +95,7 @@ class DownloadService : Service() {
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, DOWNLOAD_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
@@ -270,7 +269,7 @@ class DownloadService : Service() {
fun closeDownloadSession() {
Logger.i(TAG, "closeDownloadSession");
stopForeground(STOP_FOREGROUND_REMOVE);
stopForeground(STOP_FOREGROUND_DETACH);
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
stopService();
_started = false;
@@ -36,7 +36,6 @@ class ExportingService : Service() {
private val EXPORT_NOTIF_ID = 4;
private val EXPORT_NOTIF_TAG = "export";
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
//Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
@@ -89,7 +88,7 @@ class ExportingService : Service() {
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
@@ -188,7 +187,7 @@ class ExportingService : Service() {
fun closeExportSession() {
Logger.i(TAG, "closeExportSession");
stopForeground(STOP_FOREGROUND_REMOVE);
stopForeground(STOP_FOREGROUND_DETACH);
_notificationManager?.cancel(EXPORT_NOTIF_ID);
stopService();
_started = false;
@@ -54,9 +54,6 @@ import kotlin.system.measureTimeMillis
class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
val sessionId = UUID.randomUUID().toString();
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
val generalUri = Settings.instance.storage.getStorageGeneralUri();
if(isValidStorageUri(context, generalUri))
@@ -333,7 +330,7 @@ class StateApp {
suspend fun backgroundStarting(context: Context, scope: CoroutineScope, withFiles: Boolean, withPlugins: Boolean) {
if(contextOrNull == null) {
Logger.i(TAG, "BACKGROUND STATE: Starting");
if(!Logger.hasConsumers && (BuildConfig.DEBUG)) {
if(!Logger.hasConsumers && BuildConfig.DEBUG) {
Logger.i(TAG, "BACKGROUND STATE: Initialize logger");
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
}
@@ -10,7 +10,6 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.copyTo
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
@@ -18,7 +17,6 @@ import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
@@ -60,19 +58,6 @@ class StateBackup {
StatePlaylists.instance.toMigrateCheck()
).flatten();
fun getCache(): ImportCache {
val allPlaylists = StatePlaylists.instance.getPlaylists();
val videos = allPlaylists.flatMap { it.videos }.distinctBy { it.url };
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
val channels = allSubscriptions.map { it.channel };
return ImportCache(
videos = videos,
channels = channels
);
}
private fun getAutomaticBackupPassword(customPassword: String? = null): String {
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
@@ -248,10 +233,11 @@ class StateBackup {
.associateBy { it.config.id }
.mapValues { it.value.config.sourceUrl!! };
val cache = getCache();
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
//export.videoCache = StatePlaylists.instance.getHistory()
// .distinctBy { it.video.url }
// .map { it.video };
return export;
}
@@ -338,7 +324,7 @@ class StateBackup {
continue;
}
withContext(Dispatchers.Main) {
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value) {
synchronized(toAwait) {
toAwait.remove(store.key);
if(toAwait.isEmpty())
@@ -467,8 +453,8 @@ class StateBackup {
val stores: Map<String, List<String>>,
val plugins: Map<String, String>,
val pluginSettings: Map<String, Map<String, String?>>,
var cache: ImportCache? = null
) {
var videoCache: List<SerializedPlatformVideo>? = null;
fun asZip(): ByteArray {
return ByteArrayOutputStream().use { byteStream ->
@@ -492,17 +478,6 @@ class StateBackup {
zipStream.putNextEntry(ZipEntry("plugin_settings"));
zipStream.write(Json.encodeToString(pluginSettings).toByteArray());
if(cache != null) {
if(cache?.videos != null) {
zipStream.putNextEntry(ZipEntry("cache_videos"));
zipStream.write(Json.encodeToString(cache!!.videos).toByteArray());
}
if(cache?.channels != null) {
zipStream.putNextEntry(ZipEntry("cache_channels"));
zipStream.write(Json.encodeToString(cache!!.channels).toByteArray());
}
}
};
return byteStream.toByteArray();
}
@@ -517,8 +492,6 @@ class StateBackup {
val stores: MutableMap<String, List<String>> = mutableMapOf();
var plugins: Map<String, String> = mapOf();
var pluginSettings: Map<String, Map<String, String?>> = mapOf();
var videoCache: List<SerializedPlatformVideo>? = null
var channelCache: List<SerializedChannel>? = null
while (zipStream.nextEntry.also { entry = it } != null) {
if(entry!!.isDirectory)
@@ -530,22 +503,6 @@ class StateBackup {
"settings" -> settings = String(zipStream.readBytes());
"plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes()));
"plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes()));
"cache_videos" -> {
try {
videoCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize video cache", ex);
}
};
"cache_channels" -> {
try {
channelCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize channel cache", ex);
}
};
}
else
stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes()));
@@ -554,10 +511,7 @@ class StateBackup {
throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}");
}
}
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings, ImportCache(
videos = videoCache,
channels = channelCache
));
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings);
}
}
}
@@ -97,9 +97,6 @@ class StateDownloads {
}
}
fun getWatchLaterDescriptor(): PlaylistDownloadDescriptor? {
return _downloadPlaylists.getItems().find { it.id == VideoDownload.GROUP_WATCHLATER };
}
fun getCachedPlaylists(): List<PlaylistDownloaded> {
return _downloadPlaylists.getItems()
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
@@ -127,32 +124,19 @@ class StateDownloads {
val pdl = getPlaylistDownload(id);
if(pdl != null)
_downloadPlaylists.delete(pdl);
if(id == VideoDownload.GROUP_WATCHLATER) {
getDownloading().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
.forEach { removeDownload(it) };
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
.forEach { deleteCachedVideo(it.id) };
}
else {
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { removeDownload(it) };
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { deleteCachedVideo(it.id) };
}
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { removeDownload(it) };
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { deleteCachedVideo(it.id) };
}
fun getDownloadedVideos(): List<VideoLocal> {
return _downloaded.getItems();
}
fun getDownloadedVideosPlaylist(str: String): List<VideoLocal> {
val videos = _downloaded.findItems { it.groupID == str };
return videos;
}
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
return _downloadPlaylists.getItems();
}
fun isPlaylistCached(id: String): Boolean {
return getDownloadPlaylists().any{it.id == id};
}
@@ -193,21 +177,6 @@ class StateDownloads {
DownloadService.getOrCreateService(it);
}
}
fun checkForOutdatedPlaylistVideos(playlistId: String) {
val playlistVideos = if(playlistId == VideoDownload.GROUP_WATCHLATER)
(if(getWatchLaterDescriptor() != null) StatePlaylists.instance.getWatchLater() else listOf())
else
getCachedPlaylist(playlistId)?.playlist?.videos ?: return;
val playlistVideosDownloaded = getDownloadedVideosPlaylist(playlistId);
val urls = playlistVideos.map { it.url }.toHashSet();
for(item in playlistVideosDownloaded) {
if(!urls.contains(item.url)) {
Logger.i(TAG, "Playlist [${playlistId}] deleting removed video [${item.name}]");
deleteCachedVideo(item.id);
}
}
}
fun checkForOutdatedPlaylists(): Boolean {
var hasChanged = false;
val playlistsDownloaded = getCachedPlaylists();
@@ -223,59 +192,9 @@ class StateDownloads {
else
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
}
val downloadWatchLater = getWatchLaterDescriptor();
if(downloadWatchLater != null) {
continueDownloadWatchLater(downloadWatchLater);
}
return hasChanged;
}
fun continueDownloadWatchLater(playlistDownload: PlaylistDownloadDescriptor) {
var hasNew = false;
val watchLater = StatePlaylists.instance.getWatchLater();
for(item in watchLater) {
val existing = getCachedVideo(item.id);
if(!playlistDownload.shouldDownload(item)) {
Logger.i(TAG, "Not downloading for watchlater [${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) {
Logger.i(TAG, "New watchlater video (already downloading) ${item.name}");
ongoingDownload.groupID = VideoDownload.GROUP_WATCHLATER;
ongoingDownload.groupType = VideoDownload.GROUP_WATCHLATER;
}
else {
Logger.i(TAG, "New watchlater video ${item.name}");
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
hasNew = true;
}
}
else {
Logger.i(TAG, "New watchlater video (already downloaded) ${item.name}");
if(existing.groupID == null) {
existing.groupID = VideoDownload.GROUP_WATCHLATER;
existing.groupType = VideoDownload.GROUP_WATCHLATER;
synchronized(_downloadedSet) {
_downloadedSet.add(existing.id);
}
_downloaded.save(existing);
}
}
}
if(watchLater.isNotEmpty() && Settings.instance.downloads.shouldDownload()) {
if(hasNew) {
UIDialogs.toast("Downloading [Watch Later]")
StateApp.withContext {
DownloadService.getOrCreateService(it);
}
}
onDownloadsChanged.emit();
}
}
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
var hasNew = false;
for(item in playlist.videos) {
@@ -321,11 +240,6 @@ class StateDownloads {
onDownloadsChanged.emit();
}
}
fun downloadWatchLater(targetPixelCount: Long?, targetBitrate: Long?) {
val playlistDownload = PlaylistDownloadDescriptor(VideoDownload.GROUP_WATCHLATER, targetPixelCount, targetBitrate);
_downloadPlaylists.save(playlistDownload);
continueDownloadWatchLater(playlistDownload);
}
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
_downloadPlaylists.save(playlistDownload);
@@ -456,18 +370,6 @@ class StateDownloads {
}
}
}
try {
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
for (export in exporting)
_exporting.delete(export);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to delete dangling export:", ex);
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
}
return Pair(totalDeletedCount, totalDeleted);
}
@@ -7,7 +7,6 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
@@ -21,8 +20,8 @@ class StateHistory {
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, cache: ImportCache?): HistoryVideo
= HistoryVideo.fromReconString(backup) { url -> cache?.videos?.find { it.url == url } };
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
= HistoryVideo.fromReconString(backup, null);
})
.load();
@@ -51,9 +50,6 @@ class StateHistory {
fun getHistoryPosition(url: String): Long {
return historyIndex[url]?.position ?: 0;
}
fun isHistoryWatched(url: String, duration: Long): Boolean {
return getHistoryPosition(url) > duration * 0.7;
}
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
@@ -529,23 +529,12 @@ class StatePlatform {
}
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
return getCommonSearchCapabilitiesType(clientIds){
it.getSearchCapabilities()
};
}
fun getCommonSearchChannelContentsCapabilities(clientIds: List<String>): ResultCapabilities? {
return getCommonSearchCapabilitiesType(clientIds){
it.getSearchChannelContentsCapabilities()
};
}
fun getCommonSearchCapabilitiesType(clientIds: List<String>, capabilitiesGetter: (client: IPlatformClient)-> ResultCapabilities): ResultCapabilities? {
try {
Logger.i(TAG, "Platform - getCommonSearchCapabilities");
val clients = getEnabledClients().filter { clientIds.contains(it.id) };
val c = clients.firstOrNull() ?: return null;
val cap = capabilitiesGetter(c)//c.getSearchCapabilities();
val cap = c.getSearchCapabilities();
//var types = arrayListOf<String>();
var sorts = cap.sorts.toMutableList();
@@ -555,7 +544,7 @@ class StatePlatform {
val filtersToRemove = arrayListOf<Int>();
for (i in 1 until clients.size) {
val clientSearchCapabilities = capabilitiesGetter(clients[i]);//.getSearchCapabilities();
val clientSearchCapabilities = clients[i].getSearchCapabilities();
for (j in 0 until sorts.size) {
if (!clientSearchCapabilities.sorts.contains(sorts[j])) {
@@ -676,11 +665,8 @@ class StatePlatform {
val pagerResult: IPager<IPlatformContent>;
if(!clientCapabilities.hasType(ResultCapabilities.TYPE_MIXED) &&
( clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) ||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS) ||
clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE) ||
clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS)
)) {
clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) &&
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS)) {
val toQuery = mutableListOf<String>();
if(clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
toQuery.add(ResultCapabilities.TYPE_VIDEOS);
@@ -800,10 +786,6 @@ class StatePlatform {
return client.getChannelContents(channelUrl, type, ordering) ;
}
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
return client.peekChannelContents(channelUrl, type) ;
}
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
val channel = getChannelClient(url).getChannel(url);
@@ -14,7 +14,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.services.MediaPlaybackService
import com.futo.platformplayer.video.PlayerManager
@@ -634,7 +633,6 @@ class StatePlayer {
val instance = _instance;
_instance = null;
instance?.dispose();
Logger.i(TAG, "Disposed StatePlayer");
}
}
}
@@ -11,11 +11,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
@@ -34,10 +32,8 @@ class StatePlaylists {
.withUnique { it.url }
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): SerializedPlatformVideo
= SerializedPlatformVideo.fromVideo(
importCache?.videos?.find { it.url == backup }?.let { Logger.i(TAG, "Reconstruction [${backup}] from cache"); return@let it; } ?:
StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): SerializedPlatformVideo
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
})
.load();
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
@@ -67,10 +63,6 @@ class StatePlaylists {
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
}
}
fun removeFromWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) {
@@ -79,10 +71,6 @@ class StatePlaylists {
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
}
}
fun addToWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) {
@@ -91,8 +79,6 @@ class StatePlaylists {
_watchlistOrderStore.save();
}
onWatchLaterChanged.emit();
StateDownloads.instance.checkForOutdatedPlaylists();
}
fun getLastPlayedPlaylist() : Playlist? {
@@ -142,11 +128,6 @@ class StatePlaylists {
fun createOrUpdatePlaylist(playlist: Playlist) {
playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true);
if(playlist.id.isNotEmpty()) {
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
}
}
}
fun addToPlaylist(id: String, video: IPlatformVideo) {
synchronized(playlistStore) {
@@ -159,9 +140,6 @@ class StatePlaylists {
fun removePlaylist(playlist: Playlist) {
playlistStore.delete(playlist);
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
}
}
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
@@ -176,11 +154,7 @@ class StatePlaylists {
val reconstruction = playlistStore.getReconstructionString(playlist, true);
val newFile = File(playlistShareDir, playlist.name + ".json");
newFile.writeText(Json.encodeToString(reconstruction.split("\n") + listOf(
"__CACHE:" + Json.encodeToString(ImportCache(
videos = playlist.videos.toList()
))
)), Charsets.UTF_8);
newFile.writeText(Json.encodeToString(reconstruction.split("\n")), Charsets.UTF_8);
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
}
@@ -211,7 +185,7 @@ class StatePlaylists {
items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n");
}
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Playlist {
val items = backup.split("\n");
if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}");
@@ -220,17 +194,10 @@ class StatePlaylists {
val name = items[0];
val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try {
val videoUrl = it;
val video = importCache?.videos?.find { it.url == videoUrl } ?:
StatePlatform.instance.getContentDetails(it).await();
val video = StatePlatform.instance.getContentDetails(it).await();
if (video is IPlatformVideoDetails) {
return@map SerializedPlatformVideo.fromVideo(video);
}
else if(video is SerializedPlatformVideo) {
Logger.i(TAG, "Reconstruction [${it}] from cache");
return@map video;
}
else {
} else {
return@map null
}
}
@@ -134,11 +134,8 @@ class StatePlugins {
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
if(embeddedConfig != null) {
val existing = getPlugin(embedded.key);
if(existing == null || (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
if (existing != null)
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
else
Logger.i(TAG, "Embedded plugin nog installed [${embeddedConfig.id}] ${embeddedConfig.name} (${embeddedConfig.version}), installing");
if(existing != null && (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
installEmbeddedPlugin(context, embedded.value)
}
else if(existing != null && _isFirstEmbedUpdate) {
@@ -23,7 +23,6 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.stores.FragmentedStorage
@@ -68,40 +67,28 @@ class StatePolycentric {
return
}
for (i in 0 .. 1) {
try {
val db = SqlLiteDbHelper(context);
Store.initializeSqlLiteStore(db);
try {
val db = SqlLiteDbHelper(context);
Store.initializeSqlLiteStore(db);
val activeProcessHandleString = _activeProcessHandle.value;
if (activeProcessHandleString.isNotEmpty()) {
try {
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
} catch (e: Throwable) {
db.upgradeOldSecrets(db.writableDatabase);
val activeProcessHandleString = _activeProcessHandle.value;
if (activeProcessHandleString.isNotEmpty()) {
try {
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
} catch (e: Throwable) {
db.upgradeOldSecrets(db.writableDatabase);
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
Log.i(TAG, "Failed to initialize Polycentric.", e)
}
}
getProcessHandles()
break;
} catch (e: Throwable) {
if (i == 0) {
Logger.i(TAG, "Clearing Polycentric database due to corruption");
val db = SqlLiteDbHelper(context);
db.recreate()
} else {
_transientEnabled = false
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
Log.i(TAG, "Failed to initialize Polycentric.", e)
}
}
} catch (e: Throwable) {
_transientEnabled = false
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
Log.i(TAG, "Failed to initialize Polycentric.", e)
}
}
@@ -116,32 +103,7 @@ class StatePolycentric {
return listOf()
}
val storeProcessSecrets = Store.instance.getProcessSecrets().toMutableList()
val processSecrets = PolycentricStorage.instance.getProcessSecrets()
for (processSecret in processSecrets)
{
if (!storeProcessSecrets.contains(processSecret)) {
try {
Store.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill process secret.")
}
}
}
for (processSecret in storeProcessSecrets)
{
if (!processSecrets.contains(processSecret)) {
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill process secret.")
}
}
}
return (storeProcessSecrets + processSecrets).distinct().map { it.toProcessHandle() }
return Store.instance.getProcessSecrets().map { it.toProcessHandle(); };
}
fun setProcessHandle(processHandle: ProcessHandle?) {
@@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.functional.CentralizedFeed
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
@@ -39,8 +38,8 @@ class StateSubscriptions {
.withRestore(object: ReconstructStore<Subscription>(){
override fun toReconstruction(obj: Subscription): String =
obj.channel.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Subscription =
Subscription(importCache?.channels?.find { it.isSameUrl(backup) } ?: SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
}).load();
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
.withUnique { it.channel.url }
@@ -2,7 +2,6 @@ package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -106,7 +105,7 @@ class ManagedStore<T>{
_toReconstruct.clear();
}
}
suspend fun importReconstructions(items: List<String>, cache: ImportCache? = null, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
suspend fun importReconstructions(items: List<String>, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
var successes = 0;
val exs = ArrayList<Throwable>();
@@ -121,7 +120,7 @@ class ManagedStore<T>{
for (i in 0 .. 1) {
try {
Logger.i(TAG, "Importing ${logName(recon)}");
val reconId = createFromReconstruction(recon, builder, cache);
val reconId = createFromReconstruction(recon, builder);
successes++;
Logger.i(TAG, "Imported ${logName(reconId)}");
break;
@@ -273,12 +272,12 @@ class ManagedStore<T>{
save(obj, withReconstruction, onlyExisting);
}
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder, cache: ImportCache? = null): String {
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder): String {
if(_reconstructStore == null)
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
val id = UUID.randomUUID().toString();
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder);
save(reconstruct);
return id;
}
@@ -1,7 +1,5 @@
package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.models.ImportCache
abstract class ReconstructStore<T> {
open val backupOnSave: Boolean = false;
open val backupOnCreate: Boolean = true;
@@ -13,18 +11,18 @@ abstract class ReconstructStore<T> {
}
abstract fun toReconstruction(obj: T): String;
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache? = null): T;
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): T;
fun toReconstructionWithHeader(obj: T, fallbackName: String): String {
val identifier = identifierName ?: fallbackName;
return "@/${identifier}\n${toReconstruction(obj)}";
}
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder, importCache: ImportCache? = null): T {
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder): T {
if(backup.startsWith("@/") && backup.contains("\n"))
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder, importCache);
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder);
else
return toObject(id, backup, builder, importCache);
return toObject(id, backup, builder);
}
@@ -1,6 +1,5 @@
package com.futo.platformplayer.subscription
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.getNowDiffHours
@@ -8,7 +7,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StatePlatform
import kotlinx.coroutines.CoroutineScope
import java.time.OffsetDateTime
import java.util.concurrent.ForkJoinPool
class SmartSubscriptionAlgorithm(
@@ -72,30 +70,18 @@ class SmartSubscriptionAlgorithm(
} else {
val fetchTasks = mutableListOf<SubscriptionTask>();
val cacheTasks = mutableListOf<SubscriptionTask>();
var peekTasks = mutableListOf<SubscriptionTask>();
for(task in clientTasks.second) {
if (!task.fromCache && fetchTasks.size < limit) {
fetchTasks.add(task);
} else {
if(peekTasks.size < 100 &&
Settings.instance.subscriptions.peekChannelContents &&
(task.sub.lastPeekVideo.year < 1971 || task.sub.lastPeekVideo < task.sub.lastVideoUpdate) &&
task.client.capabilities.hasPeekChannelContents &&
task.client.getPeekChannelTypes().contains(task.type)) {
task.fromPeek = true;
task.fromCache = true;
peekTasks.add(task);
}
else {
task.fromCache = true;
cacheTasks.add(task);
}
task.fromCache = true;
cacheTasks.add(task);
}
}
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
finalTasks.addAll(fetchTasks + peekTasks + cacheTasks);
finalTasks.addAll(fetchTasks + cacheTasks);
}
}
@@ -129,9 +115,6 @@ class SmartSubscriptionAlgorithm(
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
if((type == ResultCapabilities.TYPE_MIXED || type == ResultCapabilities.TYPE_VIDEOS) && (sub.lastPeekVideo.year > 1970 && sub.lastPeekVideo > sub.lastVideoUpdate))
return 0;
else
return (expectedHours * 100).toInt();
return (expectedHours * 100).toInt();
}
}
@@ -24,7 +24,6 @@ import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import kotlinx.coroutines.CoroutineScope
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
@@ -49,17 +48,15 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val tasksGrouped = tasks.groupBy { it.client }
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } - it.value.count { it.fromPeek && it.fromCache }}), Peek(${it.value.count { it.fromPeek }})" }.joinToString("\n"));
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
try {
for(clientTasks in tasksGrouped) {
val clientTaskCount = clientTasks.value.count { !it.fromCache };
val clientCacheCount = clientTasks.value.count { it.fromCache && !it.fromPeek };
val clientPeekCount = clientTasks.value.count { it.fromPeek };
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
val clientCacheCount = clientTasks.value.size - clientTaskCount;
val limit = clientTasks.key.getSubscriptionRateLimit();
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). " +
"(${if(clientPeekCount > 0) "${clientPeekCount} peek, " else ""}${clientCacheCount} cached)");
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
}
}
@@ -138,30 +135,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
for(task in tasks) {
val forkTask = threadPool.submit<SubscriptionTaskResult> {
if(task.fromPeek) {
try {
val time = measureTimeMillis {
val peekResults = StatePlatform.instance.peekChannelContents(task.client, task.url, task.type);
val mostRecent = peekResults.firstOrNull();
task.sub.lastPeekVideo = mostRecent?.datetime ?: OffsetDateTime.MIN;
task.sub.saveAsync();
val cacheItems = peekResults.filter { it.datetime != null && it.datetime!! > task.sub.lastVideoUpdate };
//Fix for current situation
for(item in cacheItems) {
if(item.author.thumbnail.isNullOrEmpty())
item.author.thumbnail = task.sub.channel.thumbnail;
}
StateCache.instance.cacheContents(cacheItems, false);
}
Logger.i("StateSubscriptions", "Subscription peek [${task.sub.channel.name}]:${task.type} results in ${time}ms");
}
catch(ex: Throwable) {
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
}
}
synchronized(cachedChannels) {
if(task.fromCache || task.fromPeek) {
if(task.fromCache) {
finished++;
onProgress.emit(finished, forkTasks.size);
if(cachedChannels.contains(task.url)) {
@@ -243,7 +218,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val url: String,
val type: String,
var fromCache: Boolean = false,
var fromPeek: Boolean = false,
var urgency: Int = 0
);
@@ -8,13 +8,11 @@ import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.HorizontalSpaceItemDecoration
import com.futo.platformplayer.R
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
@@ -63,7 +61,6 @@ class MonetizationView : LinearLayout {
val onSupportTap = Event0();
val onStoreTap = Event0();
val onUrlTap = Event1<String>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_monetization, this);
@@ -73,12 +70,10 @@ class MonetizationView : LinearLayout {
_membershipPlatform = findViewById(R.id.membership_platform);
_buttonMembership.setOnClickListener {
_membershipUrl?.let {
/*
val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW);
intent.data = uri;
context.startActivity(intent);*/
onUrlTap.emit(it);
context.startActivity(intent);
}
}
@@ -134,18 +129,9 @@ class MonetizationView : LinearLayout {
_buttonStore.visibility = View.GONE;
}
if(profile.systemState.donationDestinations.isNotEmpty() ||
profile.systemState.membershipUrls.isNotEmpty() ||
profile.systemState.store.isNotEmpty() ||
profile.systemState.promotion.isNotEmpty())
_buttonSupport.isVisible = true;
else
_buttonSupport.isVisible = false;
_root.visibility = View.VISIBLE;
} else {
_root.visibility = View.GONE;
_buttonSupport.isVisible = false;
}
setMerchandise(null);
@@ -10,8 +10,6 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.core.view.size
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
@@ -35,13 +33,6 @@ class SupportView : LinearLayout {
private var _textNoSupportOptionsSet: TextView
private var _polycentricProfile: PolycentricProfile? = null
val hasSupportItems: Boolean get() {
return (_layoutPromotions.isVisible && _buttonPromotion.isVisible) ||
(_layoutMemberships.isVisible && _layoutMembershipEntries.isVisible && _layoutMembershipEntries.size > 0) ||
(_layoutDonation.isVisible && _layoutDonationEntries.isVisible && _layoutDonationEntries.size > 0) ||
_buttonStore.isVisible;
};
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_support, this);
@@ -20,7 +20,6 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>();
override fun getItemCount(): Int {
@@ -57,7 +56,6 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
};
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
@@ -48,7 +48,6 @@ class CommentViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>();
var onAuthorClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null
private set;
@@ -96,19 +95,6 @@ class CommentViewHolder : ViewHolder {
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
};
_creatorThumbnail.onClick.subscribe {
val c = comment ?: return@subscribe;
onAuthorClick.emit(c);
}
_creatorThumbnail.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_textAuthor.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_buttonReplies.onClick.subscribe {
val c = comment ?: return@subscribe;
onRepliesClick.emit(c);
@@ -53,10 +53,9 @@ class CommentWithReferenceViewHolder : ViewHolder {
hideLikesDislikesReplies()
}
val onRepliesClick = Event1<IPlatformComment>();
val onDelete = Event1<IPlatformComment>();
val onClick = Event1<IPlatformComment>();
val onAuthorClick = Event1<IPlatformComment>();
var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null
private set;
@@ -100,14 +99,6 @@ class CommentWithReferenceViewHolder : ViewHolder {
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
};
_creatorThumbnail.onClick.subscribe {
val c = comment ?: return@subscribe;
onAuthorClick.emit(c);
}
_textAuthor.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_buttonReplies.onClick.subscribe {
val c = comment ?: return@subscribe;
onRepliesClick.emit(c);
@@ -39,7 +39,6 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>();
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
@@ -96,7 +95,6 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
};
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
@@ -108,7 +106,6 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
};
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
@@ -164,7 +161,6 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
onChannelClicked.clear();
onAddToClicked.clear();
onAddToQueueClicked.clear();
onAddToWatchLaterClicked.clear();
}
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
@@ -19,7 +19,6 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
override val content: IPlatformContent? get() = view.content;
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
@@ -32,7 +31,6 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
view.onChannelClicked.subscribe(onChannelClicked::emit);
view.onAddToClicked.subscribe(onAddToClicked::emit);
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
}
@@ -61,7 +61,6 @@ open class PreviewVideoView : LinearLayout {
protected val _layoutDownloaded: FrameLayout;
protected val _button_add_to_queue : View;
protected val _button_add_to_watch_later : View;
protected val _button_add_to : View;
protected val _exoPlayer: PlayerManager?;
@@ -81,7 +80,6 @@ open class PreviewVideoView : LinearLayout {
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
var currentVideo: IPlatformVideo? = null
private set
@@ -106,7 +104,6 @@ open class PreviewVideoView : LinearLayout {
_containerDuration = findViewById(R.id.thumbnail_duration_container);
_containerLive = findViewById(R.id.thumbnail_live_container);
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
_button_add_to = findViewById(R.id.button_add_to);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_layoutDownloaded = findViewById(R.id.layout_downloaded);
@@ -127,7 +124,7 @@ open class PreviewVideoView : LinearLayout {
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
}
protected open fun inflate(feedStyle: FeedStyle) {
@@ -18,7 +18,6 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
val onLongPress = Event1<IPlatformVideo>();
//val context: Context;
@@ -35,7 +34,6 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
view.onChannelClicked.subscribe(onChannelClicked::emit);
view.onAddToClicked.subscribe(onAddToClicked::emit);
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
view.onLongPress.subscribe(onLongPress::emit);
}
@@ -9,16 +9,16 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.PlaylistDownloaded
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
class PlaylistDownloadItem(context: Context, val playlist: PlaylistDownloaded): LinearLayout(context) {
init { inflate(context, R.layout.list_downloaded_playlist, this) }
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
init {
imageText.text = playlistName;
imageText.text = playlist.playlist.name;
Glide.with(imageView)
.load(playlistThumbnail)
.load(playlist.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail())
.crossfade()
.into(imageView);
}
@@ -51,11 +51,9 @@ class RepliesOverlay : LinearLayout {
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
private val _loaderOverlay: LoaderOverlay
private val _client = ManagedHttpClient()
private val _layoutItems: LinearLayout
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_replies, this)
_layoutItems = findViewById(R.id.layout_items)
_topbar = findViewById(R.id.topbar);
_commentsList = findViewById(R.id.comments_list);
_addCommentView = findViewById(R.id.add_comment_view);
@@ -67,9 +65,6 @@ class RepliesOverlay : LinearLayout {
_loaderOverlay = findViewById(R.id.loader_overlay)
setLoading(false);
_layoutItems.removeView(_layoutParentComment)
_commentsList.setPrependedView(_layoutParentComment)
_addCommentView.onCommentAdded.subscribe {
_commentsList.addComment(it);
_onCommentAdded?.invoke(it);
@@ -14,10 +14,6 @@ class SupportOverlay : LinearLayout {
private val _topbar: OverlayTopbar;
private val _support: SupportView;
val hasSupportItems: Boolean get() {
return _support.hasSupportItems;
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_support, this)
_topbar = findViewById(R.id.topbar);
@@ -1,38 +0,0 @@
package com.futo.platformplayer.views.overlays
import android.content.Context
import android.util.AttributeSet
import android.webkit.WebView
import android.widget.LinearLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.SupportView
class WebviewOverlay : LinearLayout {
val onClose = Event0();
private val _topbar: OverlayTopbar;
private val _webview: WebView;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_webview, this)
_topbar = findViewById(R.id.topbar);
_webview = findViewById(R.id.webview);
_webview.settings.javaScriptEnabled = true;
_topbar.onClose.subscribe(this, onClose::emit);
}
fun goto(url: String) {
Logger.i("WebviewOverlay", "Loading [${url}]");
_topbar.setInfo(url, "");
_webview.loadUrl(url);
}
fun cleanup() {
_topbar.onClose.remove(this);
}
}
@@ -28,17 +28,13 @@ class SlideUpMenuFilters {
private var _changed: Boolean = false;
private val _lifecycleScope: CoroutineScope;
private var _isChannelSearch = false;
var commonCapabilities: ResultCapabilities? = null;
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false) {
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>) {
_lifecycleScope = lifecycleScope;
_container = container;
_enabledClientsIds = enabledClientsIds;
_filterValues = filterValues;
_isChannelSearch = isChannelSearch;
_slideUpMenuOverlay = SlideUpMenuOverlay(_container.context, _container, container.context.getString(R.string.filters), container.context.getString(R.string.done), true, listOf());
_slideUpMenuOverlay.onOK.subscribe {
onOK.emit(_enabledClientsIds, _changed);
@@ -51,10 +47,7 @@ class SlideUpMenuFilters {
private fun updateCommonCapabilities() {
_lifecycleScope.launch(Dispatchers.IO) {
try {
val caps = if(!_isChannelSearch)
StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
else
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(_enabledClientsIds);
val caps = StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
synchronized(_filterValues) {
if (caps != null) {
val keysToRemove = arrayListOf<String>();
@@ -1,38 +0,0 @@
package com.futo.platformplayer.views.overlays.slideup
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
class SlideUpMenuRecycler<T : Any, VType : AnyAdapter.AnyViewHolder<T>> : LinearLayout {
private lateinit var recyclerView: RecyclerView;
private val adapter: AnyAdapterView<T, VType>?;
var groupTag: Any? = null;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
init();
adapter = null;
}
constructor(context: Context, tag: Any, creation: (RecyclerView)->AnyAdapterView<T, VType>) : super(context){
init();
groupTag = tag;
adapter = creation(recyclerView);
}
private fun init(){
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_recycler, this, true);
recyclerView = findViewById(R.id.slide_up_menu_recycler);
}
}
@@ -71,9 +71,6 @@ class CommentsList : ConstraintLayout {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy);
onScrolled();
val totalScrollDistance = recyclerView.computeVerticalScrollOffset()
_layoutScrollToTop.visibility = if (totalScrollDistance > recyclerView.height) View.VISIBLE else View.GONE
}
};
@@ -85,23 +82,14 @@ class CommentsList : ConstraintLayout {
private var _loading = false;
private val _prependedView: FrameLayout;
private var _readonly: Boolean = false;
private val _layoutScrollToTop: FrameLayout;
var onRepliesClick = Event1<IPlatformComment>();
var onAuthorClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
_recyclerComments = findViewById(R.id.recycler_comments);
_layoutScrollToTop = findViewById(R.id.layout_scroll_to_top);
_layoutScrollToTop.setOnClickListener {
_recyclerComments.smoothScrollToPosition(0)
}
_layoutScrollToTop.visibility = View.GONE
_textMessage = TextView(context).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 30, 0, 0)
@@ -121,7 +109,6 @@ class CommentsList : ConstraintLayout {
childViewHolderFactory = { viewGroup, _ ->
val holder = CommentViewHolder(viewGroup);
holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
holder.onAuthorClick.subscribe { c -> onAuthorClick.emit(c) };
holder.onDelete.subscribe(::onDelete);
return@InsertedViewAdapterWithLoader holder;
}
@@ -582,7 +582,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoControls_fullscreen.show();
videoControls.hideImmediately();
videoControls.visibility = View.GONE;
}
else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
@@ -595,7 +594,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls.show();
_videoControls_fullscreen.hideImmediately();
_videoControls_fullscreen.visibility = View.GONE;
}
fitOrFill(fullScreen);
@@ -6,11 +6,13 @@ import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
import static java.lang.Math.min;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.http.ManagedHttpClient;
import com.futo.platformplayer.api.media.models.modifier.IRequest;
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
@@ -24,6 +26,7 @@ import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener;
import androidx.work.impl.utils.PackageManagerHelper;
import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
@@ -39,12 +42,16 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.NoRouteToHostException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import okhttp3.OkHttpClient;
/*
* Based on the default ExoPlayer DefaultHttpDataSource
*/
@@ -315,6 +322,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
responseMessage = connection.getResponseMessage();
} catch (IOException e) {
closeConnectionQuietly();
Log.e(TAG, "Failed to open: " + dataSpec.uri, e);
ManagedHttpClient client = new ManagedHttpClient(new OkHttpClient.Builder());
ManagedHttpClient.Response response = client.head(dataSpec.uri.toString(), new HashMap<>());
Log.i(TAG, "ManagedHttpClient response code: " + response.getCode());
throw HttpDataSourceException.createForIOException(
e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
@@ -574,12 +587,26 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
requestHeaders.put(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity");
String requestMethod = DataSpec.getStringForHttpMethod(httpMethod);
String requestUrl = url.toString();
if (requestModifier != null) {
IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders, requestMethod, httpBody != null ? new String(httpBody, StandardCharsets.UTF_8) : null);
String modifiedUrl = result.getUrl();
requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl;
requestHeaders = result.getHeaders();
if (modifiedUrl != null)
requestUrl = modifiedUrl;
Map<String, String> modifiedHeaders = result.getHeaders();
if (modifiedHeaders != null)
requestHeaders = modifiedHeaders;
String modifiedMethod = result.getMethod();
if (modifiedMethod != null)
requestMethod = modifiedMethod;
String modifiedBody = result.getBody();
if (modifiedBody != null)
httpBody = modifiedBody.getBytes(StandardCharsets.UTF_8);
}
HttpURLConnection connection = openConnection(new URL(requestUrl));
@@ -592,7 +619,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
connection.setRequestMethod(requestMethod);
if (httpBody != null) {
connection.setFixedLengthStreamingMode(httpBody.length);
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z"/>
</vector>
@@ -13,7 +13,7 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/foreground" />
app:srcCompat="@drawable/ic_construction" />
<!--<ImageButton
android:layout_width="wrap_content"
@@ -22,19 +22,17 @@
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_futo_logo_text" />-->
<!--
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textSize="22dp"
android:layout_marginTop="-2dp"
android:fontFamily="@font/inter_light"
android:text="Grayjay"
android:textSize="28dp"
android:fontFamily="@font/inter_bold"
android:text="TEST BUILD"
android:textColor="@color/white"
android:gravity="center_vertical"
android:layout_marginStart="8dp"/>
android:layout_marginStart="8dp"/>-->
<!--
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
@@ -56,7 +54,7 @@
android:text="@string/construction"
android:textColor="@color/white"
android:layout_marginTop="-8dp"/>
</LinearLayout>-->
</LinearLayout>
<Space
android:layout_width="0dp"
@@ -542,12 +542,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.futo.platformplayer.views.overlays.WebviewOverlay
android:id="@+id/videodetail_container_webview"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.futo.platformplayer.views.overlays.QueueEditorOverlay
android:id="@+id/videodetail_container_queue"
android:visibility="gone"
+10 -16
View File
@@ -226,18 +226,10 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="1dp"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
@@ -250,18 +242,20 @@
<LinearLayout
android:id="@+id/button_add_to"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/edit_text_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="4dp">
<ImageView
android:layout_width="20dp"
android:layout_height="16dp"
android:paddingTop="1dp"
android:src="@drawable/ic_settings" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/options"
app:srcCompat="@drawable/ic_add_white_8dp" />
<TextView
android:layout_width="wrap_content"
@@ -262,53 +262,45 @@
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="horizontal"
android:paddingEnd="6dp">
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="1dp"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
android:paddingStart="6dp"
android:paddingTop="7dp"
android:paddingStart="6dp"
android:paddingEnd="5dp"
android:paddingBottom="3dp"
app:srcCompat="@drawable/ic_queue_16dp" />
app:srcCompat="@drawable/ic_queue_16dp"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue" />
<LinearLayout
android:id="@+id/button_add_to"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginStart="4dp"
android:background="@drawable/edit_text_background"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/edit_text_background"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:padding="4dp">
<ImageView
android:layout_width="20dp"
android:layout_height="16dp"
android:paddingTop="1dp"
android:src="@drawable/ic_settings" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_add_white_8dp"
android:layout_marginStart="4dp"
android:contentDescription="@string/options" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_light"
android:text="@string/options"
android:background="@color/transparent"
android:textSize="12dp"
android:textColor="@color/white"
android:textSize="12dp" />
android:fontFamily="@font/inter_light"
android:layout_marginEnd="4dp"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -150,11 +150,14 @@
android:padding="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:layout_width="20dp"
android:layout_height="16dp"
android:paddingTop="1dp"
android:src="@drawable/ic_settings" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_add_white_8dp"
android:background="@color/transparent"
android:layout_marginStart="4dp"
android:contentDescription="@string/options" />
<TextView
android:layout_width="wrap_content"
@@ -177,28 +180,13 @@
android:paddingBottom="2dp"
android:paddingLeft="10dp"
android:paddingRight="7dp"
android:layout_marginLeft="5dp"
android:layout_marginLeft="7dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
android:paddingLeft="9dp"
android:paddingRight="9dp"
android:layout_marginLeft="5dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/text_video_name"
android:layout_width="fill_parent"
@@ -206,7 +206,6 @@
</LinearLayout>
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content"
@@ -216,28 +215,13 @@
android:paddingBottom="2dp"
android:paddingLeft="10dp"
android:paddingRight="7dp"
android:layout_marginLeft="5dp"
android:layout_marginLeft="7dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
android:paddingLeft="9dp"
android:paddingRight="9dp"
android:layout_marginLeft="5dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/text_video_name"
android:layout_width="fill_parent"
+98 -94
View File
@@ -1,108 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
android:id="@+id/layout_items">
<com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/topbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:title="Replies"
app:metadata="3 replies"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/topbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:title="Replies"
app:metadata="3 replies" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_parent_comment"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="6dp"
android:padding="12dp"
android:background="@drawable/background_16_round_4dp">
<com.futo.platformplayer.views.comments.AddCommentView
android:id="@+id/add_comment_view"
android:layout_width="match_parent"
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/image_thumbnail"
android:layout_width="25dp"
android:layout_height="25dp"
android:contentDescription="@string/channel_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/placeholder_channel_thumbnail" />
<TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp" />
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
tools:text="ShortCircuit" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_parent_comment"
<TextView
android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:padding="12dp"
android:background="@drawable/background_16_round_4dp">
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/gray_ac"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/image_thumbnail"
android:layout_width="25dp"
android:layout_height="25dp"
android:contentDescription="@string/channel_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/placeholder_channel_thumbnail" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/text_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_regular"
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
tools:text="@string/lorem_ipsum" />
<TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
tools:text="ShortCircuit" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/gray_ac"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.comments.AddCommentView
android:id="@+id/add_comment_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
app:layout_constraintTop_toBottomOf="@id/layout_parent_comment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/text_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_regular"
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="12dp" />
</LinearLayout>
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/add_comment_view"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp" />
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
@@ -110,4 +113,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/slide_up_menu_recycler"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
@@ -16,9 +16,6 @@
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:maxLines="1"
android:ellipsize="end"
android:maxWidth="300dp"
tools:text="Queue" />
<TextView
@@ -27,21 +24,16 @@
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/text_name"
app:layout_constraintRight_toLeftOf="@id/button_container"
android:fontFamily="@font/inter_regular"
android:textColor="#ACACAC"
android:textSize="13dp"
android:layout_marginLeft="15dp"
android:layout_marginBottom="7dp"
android:layout_marginRight="45dp"
android:maxLines="1"
android:ellipsize="end"
tools:text="3 videos" />
<LinearLayout
android:id="@+id/button_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintRight_toRightOf="parent"

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