mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 04:50:19 +02:00
Working smart subscriptions, Direct url through search, channel content cache trimming, skippable and skip chapter support, reinstall button for embedded plugins
This commit is contained in:
parent
34ba44ffa4
commit
93f5260e20
@ -68,6 +68,12 @@ fun ensureNotMainThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _regexUrl = Regex("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)");
|
||||||
|
fun String.isHttpUrl(): Boolean {
|
||||||
|
return _regexUrl.matchEntire(this) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
||||||
fun String.isHexColor(): Boolean {
|
fun String.isHexColor(): Boolean {
|
||||||
return _regexHexColor.matches(this);
|
return _regexHexColor.matches(this);
|
||||||
|
@ -12,19 +12,43 @@ import com.futo.platformplayer.serializers.PlatformContentSerializer
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.toSafeFileName
|
import com.futo.platformplayer.toSafeFileName
|
||||||
import com.futo.polycentric.core.toUrl
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ChannelContentCache {
|
class ChannelContentCache {
|
||||||
|
private val _targetCacheSize = 2000;
|
||||||
val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
|
val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
|
||||||
val _channelContents = HashMap(_channelCacheDir.listFiles()
|
val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>;
|
||||||
.filter { it.isDirectory }
|
init {
|
||||||
.associate { Pair(it.name, FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, it.name, PlatformContentSerializer())
|
val allFiles = _channelCacheDir.listFiles() ?: arrayOf();
|
||||||
.withoutBackup()
|
val initializeTime = measureTimeMillis {
|
||||||
.load()) });
|
_channelContents = HashMap(allFiles
|
||||||
|
.filter { it.isDirectory }
|
||||||
|
.associate {
|
||||||
|
Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer())
|
||||||
|
.withoutBackup()
|
||||||
|
.load())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
val minDays = OffsetDateTime.now().minusDays(10);
|
||||||
|
val totalItems = _channelContents.map { it.value.count() }.sum();
|
||||||
|
val toTrim = totalItems - _targetCacheSize;
|
||||||
|
val trimmed: Int;
|
||||||
|
if(toTrim > 0) {
|
||||||
|
val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) }
|
||||||
|
.sortedByDescending { it.datetime!! }.take(toTrim);
|
||||||
|
for(content in redundantContent)
|
||||||
|
uncacheContent(content);
|
||||||
|
trimmed = redundantContent.size;
|
||||||
|
}
|
||||||
|
else trimmed = 0;
|
||||||
|
Logger.i(TAG, "ChannelContentCache time: ${initializeTime}ms channels: ${allFiles.size}, videos: ${totalItems}, trimmed: ${trimmed}, total: ${totalItems - trimmed}");
|
||||||
|
}
|
||||||
|
|
||||||
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
|
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
|
||||||
val validID = channelUrl.toSafeFileName();
|
val validID = channelUrl.toSafeFileName();
|
||||||
@ -38,7 +62,9 @@ class ChannelContentCache {
|
|||||||
return PlatformContentPager(items, Math.min(150, items.size));
|
return PlatformContentPager(items, Math.min(150, items.size));
|
||||||
}
|
}
|
||||||
fun getSubscriptionCachePager(): DedupContentPager {
|
fun getSubscriptionCachePager(): DedupContentPager {
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs.map {
|
val allUrls = subs.map {
|
||||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
||||||
if(!otherUrls.contains(it.channel.url))
|
if(!otherUrls.contains(it.channel.url))
|
||||||
@ -46,6 +72,7 @@ class ChannelContentCache {
|
|||||||
else
|
else
|
||||||
return@map otherUrls;
|
return@map otherUrls;
|
||||||
}.flatten().distinct();
|
}.flatten().distinct();
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager compiling");
|
||||||
val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
|
val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
|
||||||
|
|
||||||
val validStores = _channelContents
|
val validStores = _channelContents
|
||||||
@ -58,7 +85,11 @@ class ChannelContentCache {
|
|||||||
return DedupContentPager(PlatformContentPager(items, Math.min(150, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
return DedupContentPager(PlatformContentPager(items, Math.min(150, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cacheVideos(contents: List<IPlatformContent>): List<IPlatformContent> {
|
fun uncacheContent(content: SerializedPlatformContent) {
|
||||||
|
val store = getContentStore(content);
|
||||||
|
store?.delete(content);
|
||||||
|
}
|
||||||
|
fun cacheContents(contents: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return contents.filter { cacheContent(it) };
|
return contents.filter { cacheContent(it) };
|
||||||
}
|
}
|
||||||
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
|
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
|
||||||
@ -66,14 +97,14 @@ class ChannelContentCache {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
val channelId = content.author.url.toSafeFileName();
|
val channelId = content.author.url.toSafeFileName();
|
||||||
val store = synchronized(_channelContents) {
|
val store = getContentStore(channelId).let {
|
||||||
var channelStore = _channelContents.get(channelId);
|
if(it == null) {
|
||||||
if(channelStore == null) {
|
Logger.i(TAG, "New Channel Cache for channel ${content.author.name}");
|
||||||
Logger.i(TAG, "New Subscription Cache for channel ${content.author.name}");
|
val store = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
|
||||||
channelStore = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
|
_channelContents.put(channelId, store);
|
||||||
_channelContents.put(channelId, channelStore);
|
return@let store;
|
||||||
}
|
}
|
||||||
return@synchronized channelStore;
|
else return@let it;
|
||||||
}
|
}
|
||||||
val serialized = SerializedPlatformContent.fromContent(content);
|
val serialized = SerializedPlatformContent.fromContent(content);
|
||||||
val existing = store.findItems { it.url == content.url };
|
val existing = store.findItems { it.url == content.url };
|
||||||
@ -88,6 +119,17 @@ class ChannelContentCache {
|
|||||||
return existing.isEmpty();
|
return existing.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getContentStore(content: IPlatformContent): ManagedStore<SerializedPlatformContent>? {
|
||||||
|
val channelId = content.author.url.toSafeFileName();
|
||||||
|
return getContentStore(channelId);
|
||||||
|
}
|
||||||
|
private fun getContentStore(channelId: String): ManagedStore<SerializedPlatformContent>? {
|
||||||
|
return synchronized(_channelContents) {
|
||||||
|
var channelStore = _channelContents.get(channelId);
|
||||||
|
return@synchronized channelStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChannelCache";
|
private val TAG = "ChannelCache";
|
||||||
|
|
||||||
@ -95,10 +137,11 @@ class ChannelContentCache {
|
|||||||
private var _instance: ChannelContentCache? = null;
|
private var _instance: ChannelContentCache? = null;
|
||||||
val instance: ChannelContentCache get() {
|
val instance: ChannelContentCache get() {
|
||||||
synchronized(_lock) {
|
synchronized(_lock) {
|
||||||
if(_instance == null)
|
if(_instance == null) {
|
||||||
_instance = ChannelContentCache();
|
_instance = ChannelContentCache();
|
||||||
return _instance!!;
|
}
|
||||||
}
|
}
|
||||||
|
return _instance!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
|
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
|
||||||
@ -114,7 +157,7 @@ class ChannelContentCache {
|
|||||||
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
|
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val newCacheItems = instance.cacheVideos(results);
|
val newCacheItems = instance.cacheContents(results);
|
||||||
if(onNewCacheItem != null)
|
if(onNewCacheItem != null)
|
||||||
newCacheItems.forEach { onNewCacheItem!!(it) }
|
newCacheItems.forEach { onNewCacheItem!!(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -134,7 +177,7 @@ class ChannelContentCache {
|
|||||||
Logger.i(TAG, "Caching ${results.size} subscription results");
|
Logger.i(TAG, "Caching ${results.size} subscription results");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val newCacheItems = instance.cacheVideos(results);
|
val newCacheItems = instance.cacheContents(results);
|
||||||
if(onNewCacheItem != null)
|
if(onNewCacheItem != null)
|
||||||
newCacheItems.forEach { onNewCacheItem!!(it) }
|
newCacheItems.forEach { onNewCacheItem!!(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
|
import com.futo.platformplayer.isHttpUrl
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -143,7 +144,10 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
setQuery(it, true);
|
if(it.isHttpUrl())
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
else
|
||||||
|
setQuery(it, true);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
val filteredResults = filterResults(it);
|
val filteredResults = filterResults(it);
|
||||||
recyclerData.results.addAll(filteredResults);
|
recyclerData.results.addAll(filteredResults);
|
||||||
recyclerData.resultsUnfiltered.addAll(it);
|
recyclerData.resultsUnfiltered.addAll(it);
|
||||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
if(filteredResults.isEmpty())
|
||||||
|
loadNextPage()
|
||||||
|
else
|
||||||
|
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
|
@ -218,6 +218,13 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
BigButtonGroup(c, context.getString(R.string.authentication),
|
BigButtonGroup(c, context.getString(R.string.authentication),
|
||||||
BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) {
|
BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) {
|
||||||
logoutSource();
|
logoutSource();
|
||||||
|
},
|
||||||
|
BigButton(c, "Logout without Clear", "Logout but keep the browser cookies.\nThis allows for quick re-logging.", R.drawable.ic_logout) {
|
||||||
|
logoutSource(false);
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -286,12 +293,22 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
_sourceButtons.addView(group);
|
_sourceButtons.addView(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
||||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
}.apply {
|
}.apply {
|
||||||
this.alpha = 0.5f;
|
this.alpha = 0.5f;
|
||||||
}
|
},
|
||||||
|
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||||
|
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||||
|
reloadSource(config.id);
|
||||||
|
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
|
|
||||||
_sourceAdvancedButtons.removeAllViews();
|
_sourceAdvancedButtons.removeAllViews();
|
||||||
@ -311,7 +328,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
reloadSource(config.id);
|
reloadSource(config.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
private fun logoutSource() {
|
private fun logoutSource(clear: Boolean = true) {
|
||||||
val config = _config ?: return;
|
val config = _config ?: return;
|
||||||
|
|
||||||
StatePlugins.instance.setPluginAuth(config.id, null);
|
StatePlugins.instance.setPluginAuth(config.id, null);
|
||||||
@ -319,7 +336,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
|
|
||||||
//TODO: Maybe add a dialog option..
|
//TODO: Maybe add a dialog option..
|
||||||
if(Settings.instance.plugins.clearCookiesOnLogout) {
|
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance();
|
val cookieManager: CookieManager = CookieManager.getInstance();
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
@ -110,6 +111,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
Logger.i(TAG, "SubscriptionsFeedFragment onShown()");
|
||||||
val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress();
|
val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress();
|
||||||
setProgress(currentProgress.first, currentProgress.second);
|
setProgress(currentProgress.first, currentProgress.second);
|
||||||
|
|
||||||
@ -176,6 +178,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
if(rateLimitPlugins.any())
|
if(rateLimitPlugins.any())
|
||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
|
_bypassRateLimit = false;
|
||||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
||||||
|
|
||||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
||||||
@ -279,9 +282,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
Logger.i(TAG, "Subscriptions load");
|
Logger.i(TAG, "Subscriptions load");
|
||||||
if(recyclerData.results.size == 0) {
|
if(recyclerData.results.size == 0) {
|
||||||
|
Logger.i(TAG, "Subscriptions load cache");
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||||
val results = cachePager.getResults();
|
val results = cachePager.getResults();
|
||||||
Logger.i(TAG, "Subscription show cache (${results.size})");
|
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
||||||
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
setPager(cachePager);
|
setPager(cachePager);
|
||||||
} else {
|
} else {
|
||||||
@ -291,7 +295,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
||||||
Logger.i(TAG, "Subscriptions new pager loaded");
|
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
@ -117,7 +117,10 @@ class SuggestionsFragment : MainFragment {
|
|||||||
} else if (_searchType == SearchType.PLAYLIST) {
|
} else if (_searchType == SearchType.PLAYLIST) {
|
||||||
navigate<PlaylistSearchResultsFragment>(it);
|
navigate<PlaylistSearchResultsFragment>(it);
|
||||||
} else {
|
} else {
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
if(it.isHttpUrl())
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
else
|
||||||
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
@ -173,6 +174,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _addCommentView: AddCommentView;
|
private val _addCommentView: AddCommentView;
|
||||||
private val _toggleCommentType: Toggle;
|
private val _toggleCommentType: Toggle;
|
||||||
|
|
||||||
|
private val _layoutSkip: LinearLayout;
|
||||||
|
private val _textSkip: TextView;
|
||||||
private val _textResume: TextView;
|
private val _textResume: TextView;
|
||||||
private val _layoutResume: LinearLayout;
|
private val _layoutResume: LinearLayout;
|
||||||
private var _jobHideResume: Job? = null;
|
private var _jobHideResume: Job? = null;
|
||||||
@ -296,6 +299,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
|
|
||||||
|
_layoutSkip = findViewById(R.id.layout_skip);
|
||||||
|
_textSkip = findViewById(R.id.text_skip);
|
||||||
_layoutResume = findViewById(R.id.layout_resume);
|
_layoutResume = findViewById(R.id.layout_resume);
|
||||||
_textResume = findViewById(R.id.text_resume);
|
_textResume = findViewById(R.id.text_resume);
|
||||||
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
|
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
|
||||||
@ -403,6 +408,21 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
||||||
_player.onVideoSettings.subscribe { showVideoSettings() };
|
_player.onVideoSettings.subscribe { showVideoSettings() };
|
||||||
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
||||||
|
_player.onChapterChanged.subscribe { chapter, isScrub ->
|
||||||
|
if(_layoutSkip.visibility == VISIBLE && chapter?.type != ChapterType.SKIPPABLE)
|
||||||
|
_layoutSkip.visibility = GONE;
|
||||||
|
|
||||||
|
if(!isScrub) {
|
||||||
|
if(chapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
_layoutSkip.visibility = VISIBLE;
|
||||||
|
}
|
||||||
|
else if(chapter?.type == ChapterType.SKIP) {
|
||||||
|
_player.seekTo(chapter.timeEnd.toLong() * 1000);
|
||||||
|
UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_cast.onMinimizeClick.subscribe {
|
_cast.onMinimizeClick.subscribe {
|
||||||
_player.setFullScreen(false);
|
_player.setFullScreen(false);
|
||||||
onMinimize.emit();
|
onMinimize.emit();
|
||||||
@ -581,6 +601,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_layoutSkip.setOnClickListener {
|
||||||
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
_player.seekTo(currentChapter.timeEnd.toLong() * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
@ -961,9 +988,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||||
|
@ -33,6 +33,7 @@ import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
@ -54,6 +55,8 @@ import java.io.File
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* This class contains global context for unconventional cases where obtaining context is hard.
|
* This class contains global context for unconventional cases where obtaining context is hard.
|
||||||
@ -380,6 +383,18 @@ class StateApp {
|
|||||||
fun mainAppStarted(context: Context) {
|
fun mainAppStarted(context: Context) {
|
||||||
Logger.i(TAG, "App started");
|
Logger.i(TAG, "App started");
|
||||||
|
|
||||||
|
//Start loading cache
|
||||||
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
ChannelContentCache.instance;
|
||||||
|
}
|
||||||
|
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to load announcements.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
||||||
|
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||||
@ -441,7 +456,7 @@ class StateApp {
|
|||||||
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||||
if (isRateLimitReached) {
|
if (isRateLimitReached) {
|
||||||
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||||
delay(5000);
|
delay(8000);
|
||||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -108,14 +108,12 @@ class StatePlugins {
|
|||||||
instance.deletePlugin(embedded.key);
|
instance.deletePlugin(embedded.key);
|
||||||
StatePlatform.instance.updateAvailableClients(context);
|
StatePlatform.instance.updateAvailableClients(context);
|
||||||
}
|
}
|
||||||
fun updateEmbeddedPlugins(context: Context) {
|
fun updateEmbeddedPlugins(context: Context, subset: List<String>? = null, force: Boolean = false) {
|
||||||
for(embedded in getEmbeddedSources(context)) {
|
for(embedded in getEmbeddedSources(context).filter { subset == null || subset.contains(it.key) }) {
|
||||||
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
||||||
if(FORCE_REINSTALL_EMBEDDED)
|
if(embeddedConfig != null) {
|
||||||
deletePlugin(embedded.key);
|
|
||||||
else if(embeddedConfig != null) {
|
|
||||||
val existing = getPlugin(embedded.key);
|
val existing = getPlugin(embedded.key);
|
||||||
if(existing != null && existing.config.version < embeddedConfig.version ) {
|
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");
|
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig?.version}), reinstalling");
|
||||||
//deletePlugin(embedded.key);
|
//deletePlugin(embedded.key);
|
||||||
installEmbeddedPlugin(context, embedded.value)
|
installEmbeddedPlugin(context, embedded.value)
|
||||||
|
@ -242,8 +242,12 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val usePolycentric = false;
|
||||||
val subUrls = getSubscriptions().associateWith {
|
val subUrls = getSubscriptions().associateWith {
|
||||||
StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id)
|
if(usePolycentric)
|
||||||
|
StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id);
|
||||||
|
else
|
||||||
|
listOf(it.channel.url);
|
||||||
};
|
};
|
||||||
|
|
||||||
val result = algo.getSubscriptions(subUrls);
|
val result = algo.getSubscriptions(subUrls);
|
||||||
|
@ -181,6 +181,12 @@ class ManagedStore<T>{
|
|||||||
return ReconstructionResult(successes, exs, builder.messages);
|
return ReconstructionResult(successes, exs, builder.messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun count(): Int {
|
||||||
|
synchronized(_files) {
|
||||||
|
return _files.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
fun getItems(): List<T> {
|
fun getItems(): List<T> {
|
||||||
synchronized(_files) {
|
synchronized(_files) {
|
||||||
return _files.map { it.key };
|
return _files.map { it.key };
|
||||||
|
@ -49,8 +49,11 @@ class SmartSubscriptionAlgorithm(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for(task in allTasks)
|
||||||
|
task.urgency = calculateUpdateUrgency(task.sub, task.type);
|
||||||
|
|
||||||
val ordering = allTasks.groupBy { it.client }
|
val ordering = allTasks.groupBy { it.client }
|
||||||
.map { Pair(it.key, it.value.sortedBy { calculateUpdateUrgency(it.sub, it.type) }) };
|
.map { Pair(it.key, it.value.sortedBy { it.urgency }) };
|
||||||
|
|
||||||
val finalTasks = mutableListOf<SubscriptionTask>();
|
val finalTasks = mutableListOf<SubscriptionTask>();
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ class SmartSubscriptionAlgorithm(
|
|||||||
};
|
};
|
||||||
val lastItemDaysAgo = lastItem.getNowDiffHours();
|
val lastItemDaysAgo = lastItem.getNowDiffHours();
|
||||||
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
||||||
val expectedHours = lastUpdateHoursAgo.toDouble() - (interval*24);
|
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
||||||
|
|
||||||
return (expectedHours * 100).toInt();
|
return (expectedHours * 100).toInt();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.cache.ChannelContentCache
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
@ -42,24 +43,89 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
val tasks = getSubscriptionTasks(subs);
|
val tasks = getSubscriptionTasks(subs);
|
||||||
|
|
||||||
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
val taskCount = tasks.filter { !it.fromCache }.size;
|
||||||
|
val cacheCount = tasks.size - taskCount;
|
||||||
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
||||||
" Tasks: ${tasks.filter { !it.fromCache }.size}\n" +
|
" Tasks: ${taskCount}\n" +
|
||||||
" Cached: ${tasks.filter { it.fromCache }.size}");
|
" Cached: ${cacheCount}");
|
||||||
try {
|
try {
|
||||||
//TODO: Remove this
|
for(clientTasks in tasksGrouped) {
|
||||||
UIDialogs.toast("Tasks: ${tasks.filter { !it.fromCache }.size}\n" +
|
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||||
"Cached: ${tasks.filter { it.fromCache }.size}", false);
|
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||||
|
if(clientCacheCount > 0) {
|
||||||
|
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (ex: Throwable){}
|
} catch (ex: Throwable){}
|
||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
val taskResults = arrayListOf<IPager<IPlatformContent>>();
|
|
||||||
|
|
||||||
|
val failedPlugins = mutableListOf<String>();
|
||||||
|
val cachedChannels = mutableListOf<String>()
|
||||||
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
|
|
||||||
|
|
||||||
|
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
||||||
|
val timeTotal = measureTimeMillis {
|
||||||
|
for(task in forkTasks) {
|
||||||
|
try {
|
||||||
|
val result = task.get();
|
||||||
|
if(result != null) {
|
||||||
|
if(result.pager != null)
|
||||||
|
taskResults.add(result);
|
||||||
|
else if(result.exception != null) {
|
||||||
|
val ex = result.exception;
|
||||||
|
if(ex != null) {
|
||||||
|
val nonRuntimeEx = findNonRuntimeException(ex);
|
||||||
|
if (nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
||||||
|
exs.add(nonRuntimeEx);
|
||||||
|
else
|
||||||
|
throw ex.cause ?: ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
val nonRuntimeEx = findNonRuntimeException(ex.cause);
|
||||||
|
if(nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
||||||
|
exs.add(nonRuntimeEx);
|
||||||
|
else
|
||||||
|
throw ex.cause ?: ex;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
||||||
|
|
||||||
|
//Cache pagers grouped by channel
|
||||||
|
val groupedPagers = taskResults.groupBy { it.task.sub.channel.url }
|
||||||
|
.map { entry ->
|
||||||
|
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
|
||||||
|
val liveTasks = entry.value.filter { !it.task.fromCache };
|
||||||
|
val cachedTasks = entry.value.filter { it.task.fromCache };
|
||||||
|
val livePager = if(!liveTasks.isEmpty()) ChannelContentCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, {
|
||||||
|
onNewCacheHit.emit(sub!!, it);
|
||||||
|
}) else null;
|
||||||
|
val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null;
|
||||||
|
if(livePager != null && cachedPager == null)
|
||||||
|
return@map livePager;
|
||||||
|
else if(cachedPager != null && livePager == null)
|
||||||
|
return@map cachedPager;
|
||||||
|
else if(cachedPager == null && livePager == null)
|
||||||
|
return@map EmptyPager();
|
||||||
|
else
|
||||||
|
return@map MultiChronoContentPager(listOf(livePager!!, cachedPager!!), true).apply { this.initialize() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
||||||
|
pager.initialize();
|
||||||
|
|
||||||
|
return Result(DedupContentPager(pager), exs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
|
||||||
val forkTasks = mutableListOf<ForkJoinTask<SubscriptionTaskResult>>();
|
val forkTasks = mutableListOf<ForkJoinTask<SubscriptionTaskResult>>();
|
||||||
var finished = 0;
|
var finished = 0;
|
||||||
val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf();
|
|
||||||
val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency();
|
|
||||||
val failedPlugins = arrayListOf<String>();
|
|
||||||
val cachedChannels = arrayListOf<String>();
|
|
||||||
|
|
||||||
for(task in tasks) {
|
for(task in tasks) {
|
||||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||||
@ -87,10 +153,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
pager = StatePlatform.instance.getChannelContent(task.client,
|
pager = StatePlatform.instance.getChannelContent(task.client,
|
||||||
task.url, task.type, ResultCapabilities.ORDER_CHONOLOGICAL);
|
task.url, task.type, ResultCapabilities.ORDER_CHONOLOGICAL);
|
||||||
|
|
||||||
pager = ChannelContentCache.cachePagerResults(scope, pager) {
|
|
||||||
onNewCacheHit.emit(task.sub, it);
|
|
||||||
};
|
|
||||||
|
|
||||||
val initialPage = pager.getResults();
|
val initialPage = pager.getResults();
|
||||||
task.sub.updateSubscriptionState(task.type, initialPage);
|
task.sub.updateSubscriptionState(task.type, initialPage);
|
||||||
StateSubscriptions.instance.saveSubscription(task.sub);
|
StateSubscriptions.instance.saveSubscription(task.sub);
|
||||||
@ -105,6 +167,27 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val channelEx = ChannelException(task.sub.channel, ex);
|
val channelEx = ChannelException(task.sub.channel, ex);
|
||||||
finished++;
|
finished++;
|
||||||
onProgress.emit(finished, forkTasks.size);
|
onProgress.emit(finished, forkTasks.size);
|
||||||
|
|
||||||
|
|
||||||
|
if(ex is ScriptCaptchaRequiredException) {
|
||||||
|
synchronized(failedPlugins) {
|
||||||
|
//Fail all subscription calls to plugin if it has a captcha issue
|
||||||
|
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||||
|
Logger.w(StateSubscriptions.TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to Captcha");
|
||||||
|
failedPlugins.add(ex.config.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(ex is ScriptCriticalException) {
|
||||||
|
synchronized(failedPlugins) {
|
||||||
|
//Fail all subscription calls to plugin if it has a critical issue
|
||||||
|
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||||
|
Logger.w(StateSubscriptions.TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message);
|
||||||
|
failedPlugins.add(ex.config.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!withCacheFallback)
|
if (!withCacheFallback)
|
||||||
throw channelEx;
|
throw channelEx;
|
||||||
else {
|
else {
|
||||||
@ -117,39 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
}
|
}
|
||||||
forkTasks.add(forkTask);
|
forkTasks.add(forkTask);
|
||||||
}
|
}
|
||||||
|
return forkTasks;
|
||||||
val timeTotal = measureTimeMillis {
|
|
||||||
for(task in forkTasks) {
|
|
||||||
try {
|
|
||||||
val result = task.get();
|
|
||||||
if(result != null) {
|
|
||||||
if(result.pager != null)
|
|
||||||
taskResults.add(result.pager!!);
|
|
||||||
if(exceptionMap.containsKey(result.task.sub)) {
|
|
||||||
val ex = exceptionMap[result.task.sub];
|
|
||||||
if(ex != null) {
|
|
||||||
val nonRuntimeEx = findNonRuntimeException(ex);
|
|
||||||
if (nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
|
||||||
exs.add(nonRuntimeEx);
|
|
||||||
else
|
|
||||||
throw ex.cause ?: ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: ExecutionException) {
|
|
||||||
val nonRuntimeEx = findNonRuntimeException(ex.cause);
|
|
||||||
if(nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
|
||||||
exs.add(nonRuntimeEx);
|
|
||||||
else
|
|
||||||
throw ex.cause ?: ex;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
|
||||||
val pager = MultiChronoContentPager(taskResults, allowFailure, 15);
|
|
||||||
pager.initialize();
|
|
||||||
|
|
||||||
return Result(DedupContentPager(pager), exs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask>;
|
abstract fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask>;
|
||||||
@ -160,7 +211,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val sub: Subscription,
|
val sub: Subscription,
|
||||||
val url: String,
|
val url: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
var fromCache: Boolean = false
|
var fromCache: Boolean = false,
|
||||||
|
var urgency: Int = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
class SubscriptionTaskResult(
|
class SubscriptionTaskResult(
|
||||||
|
@ -100,6 +100,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>();
|
val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>();
|
||||||
val onSourceEnded = Event0();
|
val onSourceEnded = Event0();
|
||||||
|
|
||||||
|
val onChapterChanged = Event2<IChapter?, Boolean>();
|
||||||
|
|
||||||
val onVideoClicked = Event0();
|
val onVideoClicked = Event0();
|
||||||
val onTimeBarChanged = Event2<Long, Long>();
|
val onTimeBarChanged = Event2<Long, Long>();
|
||||||
|
|
||||||
@ -185,6 +187,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||||
gestureControl.restartHideJob();
|
gestureControl.restartHideJob();
|
||||||
|
|
||||||
|
updateCurrentChapter(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||||
@ -233,17 +237,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
val delta = position - lastPos;
|
val delta = position - lastPos;
|
||||||
if(delta > 1000 || delta < 0) {
|
if(delta > 1000 || delta < 0) {
|
||||||
lastPos = position;
|
lastPos = position;
|
||||||
val currentChapter = getCurrentChapter(position)
|
updateCurrentChapter();
|
||||||
if(_currentChapter != currentChapter) {
|
|
||||||
_currentChapter = currentChapter;
|
|
||||||
if (currentChapter != null) {
|
|
||||||
_control_chapter.text = " • " + currentChapter.name;
|
|
||||||
_control_chapter_fullscreen.text = " • " + currentChapter.name;
|
|
||||||
} else {
|
|
||||||
_control_chapter.text = "";
|
|
||||||
_control_chapter_fullscreen.text = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,6 +250,22 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
exoPlayer?.attach(_videoView, PLAYER_STATE_NAME);
|
exoPlayer?.attach(_videoView, PLAYER_STATE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCurrentChapter(pos: Long? = null) {
|
||||||
|
val chaptPos = pos ?: position;
|
||||||
|
val currentChapter = getCurrentChapter(chaptPos);
|
||||||
|
if(_currentChapter != currentChapter) {
|
||||||
|
_currentChapter = currentChapter;
|
||||||
|
if (currentChapter != null) {
|
||||||
|
_control_chapter.text = " • " + currentChapter.name;
|
||||||
|
_control_chapter_fullscreen.text = " • " + currentChapter.name;
|
||||||
|
} else {
|
||||||
|
_control_chapter.text = "";
|
||||||
|
_control_chapter_fullscreen.text = "";
|
||||||
|
}
|
||||||
|
onChapterChanged.emit(currentChapter, pos != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setArtwork(drawable: Drawable?) {
|
fun setArtwork(drawable: Drawable?) {
|
||||||
if (drawable != null) {
|
if (drawable != null) {
|
||||||
_videoView.defaultArtwork = drawable;
|
_videoView.defaultArtwork = drawable;
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="60dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingTop="6dp"
|
|
||||||
android:paddingBottom="7dp"
|
|
||||||
android:paddingStart="7dp"
|
android:paddingStart="7dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:background="@drawable/background_big_button"
|
android:background="@drawable/background_big_button"
|
||||||
@ -26,6 +24,8 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/button_text"
|
android:id="@+id/button_text"
|
||||||
@ -47,7 +47,7 @@
|
|||||||
android:textSize="12dp"
|
android:textSize="12dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:fontFamily="@font/inter_extra_light"
|
android:fontFamily="@font/inter_extra_light"
|
||||||
android:maxLines="1"
|
android:maxLines="2"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
tools:text="Attempts to fetch your subscriptions from this source" />
|
tools:text="Attempts to fetch your subscriptions from this source" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -150,6 +150,30 @@
|
|||||||
android:textSize="12dp"
|
android:textSize="12dp"
|
||||||
android:fontFamily="@font/inter_light" />
|
android:fontFamily="@font/inter_light" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_skip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="5dp"
|
||||||
|
android:background="@drawable/background_button_transparent_round"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_skip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Skip"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:fontFamily="@font/inter_light" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/contentContainer"
|
android:id="@+id/contentContainer"
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 5011bfcddb084007b938e6276b11f63e940006eb
|
Subproject commit ce7d9d0bc7cd18c4f50338e30a82ca132e24f70d
|
Loading…
x
Reference in New Issue
Block a user