diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt
index 0a17d96142345d29e78c198529b03cc434dca489..3d93437c6941672436807b779a58d7844ff0bd5d 100644
--- a/app/src/main/java/com/futo/platformplayer/Utility.kt
+++ b/app/src/main/java/com/futo/platformplayer/Utility.kt
@@ -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})");
 fun String.isHexColor(): Boolean {
     return _regexHexColor.matches(this);
diff --git a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
index d0ff0b4f31a13f0cde42cf2f9e06b44dd4c18ebd..1464b035c7a1f0e7494844de93d8f5f527544dd7 100644
--- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
+++ b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
@@ -12,19 +12,43 @@ import com.futo.platformplayer.serializers.PlatformContentSerializer
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.states.StateSubscriptions
 import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.stores.v2.ManagedStore
 import com.futo.platformplayer.toSafeFileName
-import com.futo.polycentric.core.toUrl
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import java.time.OffsetDateTime
+import kotlin.system.measureTimeMillis
 
 class ChannelContentCache {
+    private val _targetCacheSize = 2000;
     val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
-    val _channelContents = HashMap(_channelCacheDir.listFiles()
-        .filter { it.isDirectory }
-        .associate { Pair(it.name, FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, it.name, PlatformContentSerializer())
-            .withoutBackup()
-            .load()) });
+    val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>;
+    init {
+        val allFiles = _channelCacheDir.listFiles() ?: arrayOf();
+        val initializeTime = measureTimeMillis {
+            _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 {
         val validID = channelUrl.toSafeFileName();
@@ -38,7 +62,9 @@ class ChannelContentCache {
         return PlatformContentPager(items, Math.min(150, items.size));
     }
     fun getSubscriptionCachePager(): DedupContentPager {
+        Logger.i(TAG, "Subscriptions CachePager get subscriptions");
         val subs = StateSubscriptions.instance.getSubscriptions();
+        Logger.i(TAG, "Subscriptions CachePager polycentric urls");
         val allUrls = subs.map {
             val otherUrls =  PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
             if(!otherUrls.contains(it.channel.url))
@@ -46,6 +72,7 @@ class ChannelContentCache {
             else
                 return@map otherUrls;
         }.flatten().distinct();
+        Logger.i(TAG, "Subscriptions CachePager compiling");
         val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
 
         val validStores = _channelContents
@@ -58,7 +85,11 @@ class ChannelContentCache {
         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) };
     }
     fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
@@ -66,14 +97,14 @@ class ChannelContentCache {
             return false;
 
         val channelId = content.author.url.toSafeFileName();
-        val store = synchronized(_channelContents) {
-            var channelStore = _channelContents.get(channelId);
-            if(channelStore == null) {
-                Logger.i(TAG, "New Subscription Cache for channel ${content.author.name}");
-                channelStore = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
-                _channelContents.put(channelId, channelStore);
+        val store = getContentStore(channelId).let {
+            if(it == null) {
+                Logger.i(TAG, "New Channel Cache for channel ${content.author.name}");
+                val store = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
+                _channelContents.put(channelId, store);
+                return@let store;
             }
-            return@synchronized channelStore;
+            else return@let it;
         }
         val serialized = SerializedPlatformContent.fromContent(content);
         val existing = store.findItems { it.url == content.url };
@@ -88,6 +119,17 @@ class ChannelContentCache {
         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 {
         private val TAG = "ChannelCache";
 
@@ -95,10 +137,11 @@ class ChannelContentCache {
         private var _instance: ChannelContentCache? = null;
         val instance: ChannelContentCache get() {
             synchronized(_lock) {
-                if(_instance == null)
+                if(_instance == null) {
                     _instance = ChannelContentCache();
-                return _instance!!;
+                }
             }
+            return _instance!!;
         }
 
         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()}]");
             scope.launch(Dispatchers.IO) {
                 try {
-                    val newCacheItems = instance.cacheVideos(results);
+                    val newCacheItems = instance.cacheContents(results);
                     if(onNewCacheItem != null)
                         newCacheItems.forEach { onNewCacheItem!!(it) }
                 } catch (e: Throwable) {
@@ -134,7 +177,7 @@ class ChannelContentCache {
             Logger.i(TAG, "Caching ${results.size} subscription results");
             scope.launch(Dispatchers.IO) {
                 try {
-                    val newCacheItems = instance.cacheVideos(results);
+                    val newCacheItems = instance.cacheContents(results);
                     if(onNewCacheItem != null)
                         newCacheItems.forEach { onNewCacheItem!!(it) }
                 } catch (e: Throwable) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
index a22c2ab3b1cbb410a9041f52e68192dfe10239e0..6ca87a28077721dc08bb812e201e8c689a4015e6 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
@@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.structures.IPager
 import com.futo.platformplayer.constructs.TaskHandler
 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
 import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
+import com.futo.platformplayer.isHttpUrl
 import com.futo.platformplayer.views.FeedStyle
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -143,7 +144,10 @@ class ContentSearchResultsFragment : MainFragment() {
                     };
 
                     onSearch.subscribe(this) {
-                        setQuery(it, true);
+                        if(it.isHttpUrl())
+                            navigate<VideoDetailFragment>(it);
+                        else
+                            setQuery(it, true);
                     };
                 }
             }
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
index 3f0938cc0d7c4ef52e79703db0137c80945b5733..0f70aef32d77ae298eb41beefb9a9a110bceb665 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
@@ -141,7 +141,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
             val filteredResults = filterResults(it);
             recyclerData.results.addAll(filteredResults);
             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> {
             Logger.w(TAG, "Failed to load next page.", it);
             UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt
index 0693f9d8e72dd5e312753a6db844a105b6608455..07eff4105185bbb0914cad5f77c93d3528d610f3 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt
@@ -218,6 +218,13 @@ class SourceDetailFragment : MainFragment() {
                     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) {
                             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);
             }
 
+            val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
             val advancedButtons = BigButtonGroup(c, "Advanced",
                 BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
 
                 }.apply {
                     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();
@@ -311,7 +328,7 @@ class SourceDetailFragment : MainFragment() {
                 reloadSource(config.id);
             };
         }
-        private fun logoutSource() {
+        private fun logoutSource(clear: Boolean = true) {
             val config = _config ?: return;
 
             StatePlugins.instance.setPluginAuth(config.id, null);
@@ -319,7 +336,7 @@ class SourceDetailFragment : MainFragment() {
 
 
             //TODO: Maybe add a dialog option..
-            if(Settings.instance.plugins.clearCookiesOnLogout) {
+            if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
                 val cookieManager: CookieManager = CookieManager.getInstance();
                 cookieManager.removeAllCookies(null);
             }
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index f65c6931f8fe0e93d3170f9e125a326ec23691b6..e8bf35d8677323dfb524dd0db97160495db4e5a0 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -87,6 +87,7 @@ class SubscriptionsFeedFragment : MainFragment() {
     @SuppressLint("ViewConstructor")
     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) {
+            Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
             StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
                 fragment.lifecycleScope.launch(Dispatchers.Main) {
                     try {
@@ -110,6 +111,7 @@ class SubscriptionsFeedFragment : MainFragment() {
         }
 
         fun onShown() {
+            Logger.i(TAG, "SubscriptionsFeedFragment onShown()");
             val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress();
             setProgress(currentProgress.first, currentProgress.second);
 
@@ -176,6 +178,7 @@ class SubscriptionsFeedFragment : MainFragment() {
                 if(rateLimitPlugins.any())
                     throw RateLimitException(rateLimitPlugins.map { it.key.id });
             }
+            _bypassRateLimit = false;
             val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
 
             val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
@@ -279,9 +282,10 @@ class SubscriptionsFeedFragment : MainFragment() {
             setLoading(true);
             Logger.i(TAG, "Subscriptions load");
             if(recyclerData.results.size == 0) {
+                Logger.i(TAG, "Subscriptions load cache");
                 val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
                 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);
                 setPager(cachePager);
             } else {
@@ -291,7 +295,7 @@ class SubscriptionsFeedFragment : MainFragment() {
         }
 
         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) {
                 try {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
index 27a960945e03d22a852f7df164cd0cd937cba4f3..e5499062cb36b13ee456128eea5c8b7e07c14c21 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
@@ -117,7 +117,10 @@ class SuggestionsFragment : MainFragment {
                     } else if (_searchType == SearchType.PLAYLIST) {
                         navigate<PlaylistSearchResultsFragment>(it);
                     } else {
-                        navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
+                        if(it.isHttpUrl())
+                            navigate<VideoDetailFragment>(it);
+                        else
+                            navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
                     }
                 };
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index beacbf0f060646f0f7b39496758369ffb731e98d..94664802b42e4472102af20109354ffc53c0ceb0 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -38,6 +38,7 @@ import com.futo.platformplayer.api.media.LiveChatManager
 import com.futo.platformplayer.api.media.PlatformID
 import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
 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.live.ILiveChatWindowDescriptor
 import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
@@ -173,6 +174,8 @@ class VideoDetailView : ConstraintLayout {
     private val _addCommentView: AddCommentView;
     private val _toggleCommentType: Toggle;
 
+    private val _layoutSkip: LinearLayout;
+    private val _textSkip: TextView;
     private val _textResume: TextView;
     private val _layoutResume: LinearLayout;
     private var _jobHideResume: Job? = null;
@@ -296,6 +299,8 @@ class VideoDetailView : ConstraintLayout {
         _addCommentView = findViewById(R.id.add_comment_view);
         _commentsList = findViewById(R.id.comments_list);
 
+        _layoutSkip = findViewById(R.id.layout_skip);
+        _textSkip = findViewById(R.id.text_skip);
         _layoutResume = findViewById(R.id.layout_resume);
         _textResume = findViewById(R.id.text_resume);
         _layoutPlayerContainer = findViewById(R.id.layout_player_container);
@@ -403,6 +408,21 @@ class VideoDetailView : ConstraintLayout {
         _cast.onSettingsClick.subscribe { showVideoSettings() };
         _player.onVideoSettings.subscribe { showVideoSettings() };
         _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 {
             _player.setFullScreen(false);
             onMinimize.emit();
@@ -581,6 +601,13 @@ class VideoDetailView : ConstraintLayout {
 
             _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() {
@@ -961,9 +988,10 @@ class VideoDetailView : ConstraintLayout {
                 }
                 catch(ex: Throwable) {
                     Logger.e(TAG, "Failed to get chapters", ex);
-                    withContext(Dispatchers.Main) {
+
+                    /*withContext(Dispatchers.Main) {
                         UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
-                    }
+                    }*/
                 }
                 try {
                     val stopwatch = com.futo.platformplayer.debug.Stopwatch()
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 0545d2faa0d6da37aaf9d6ba011798d874e5db03..371c15ff8837a0e7e636bf0cef9489af98aa34f1 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -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.internal.JSHttpClient
 import com.futo.platformplayer.background.BackgroundWorker
+import com.futo.platformplayer.cache.ChannelContentCache
 import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
@@ -54,6 +55,8 @@ import java.io.File
 import java.time.OffsetDateTime
 import java.util.*
 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.
@@ -380,6 +383,18 @@ class StateApp {
     fun mainAppStarted(context: Context) {
         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())
 
         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 };
                 if (isRateLimitReached) {
                     Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
-                    delay(5000);
+                    delay(8000);
                     StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
                 }
                 else
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
index 27b030e1e085954a3a3b92349e502f7d6b2e8bb5..58454cc6e6b30aae4df8920643a6bf1b721e283b 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
@@ -108,14 +108,12 @@ class StatePlugins {
             instance.deletePlugin(embedded.key);
         StatePlatform.instance.updateAvailableClients(context);
     }
-    fun updateEmbeddedPlugins(context: Context) {
-        for(embedded in getEmbeddedSources(context)) {
+    fun updateEmbeddedPlugins(context: Context, subset: List<String>? = null, force: Boolean = false) {
+        for(embedded in getEmbeddedSources(context).filter { subset == null || subset.contains(it.key) }) {
             val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
-            if(FORCE_REINSTALL_EMBEDDED)
-                deletePlugin(embedded.key);
-            else if(embeddedConfig != null) {
+            if(embeddedConfig != null) {
                 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");
                     //deletePlugin(embedded.key);
                     installEmbeddedPlugin(context, embedded.value)
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
index a6274d266976cf60811cd1841bdaeee846fc9805..b686e659cedd14d91c066fe38c9448b3eb724890 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
@@ -242,8 +242,12 @@ class StateSubscriptions {
 
         }
 
+        val usePolycentric = false;
         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);
diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt
index a76af3a78a83fdb6ac0c877b36287877c90ba3b2..596f3be24d8b464aef531c5e5ace941a639e8c1a 100644
--- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt
+++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt
@@ -181,6 +181,12 @@ class ManagedStore<T>{
         return ReconstructionResult(successes, exs, builder.messages);
     }
 
+
+    fun count(): Int {
+        synchronized(_files) {
+            return _files.size;
+        }
+    }
     fun getItems(): List<T> {
         synchronized(_files) {
             return _files.map { it.key };
diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt
index 60e2bb5ee85aa492da1fd9472dcb989fbe06f93d..9132f6db22f5fb1bf0b6fd41923da04f131e565f 100644
--- a/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt
+++ b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt
@@ -49,8 +49,11 @@ class SmartSubscriptionAlgorithm(
                 };
         };
 
+        for(task in allTasks)
+            task.urgency = calculateUpdateUrgency(task.sub, task.type);
+
         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>();
 
@@ -100,7 +103,7 @@ class SmartSubscriptionAlgorithm(
         };
         val lastItemDaysAgo = lastItem.getNowDiffHours();
         val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
-        val expectedHours = lastUpdateHoursAgo.toDouble() - (interval*24);
+        val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
 
         return (expectedHours * 100).toInt();
     }
diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
index 2a9de5bc17659fc54d3b14555c3105e4b73c696d..1b70d014a2e5b3228135c8a902ab3fad8e11f827 100644
--- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
+++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
@@ -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.SourcePluginConfig
 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.MultiChronoContentPager
 import com.futo.platformplayer.cache.ChannelContentCache
@@ -42,24 +43,89 @@ abstract class SubscriptionsTaskFetchAlgorithm(
     override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
         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" +
-            "   Tasks: ${tasks.filter { !it.fromCache }.size}\n" +
-            "   Cached: ${tasks.filter { it.fromCache }.size}");
+            "   Tasks: ${taskCount}\n" +
+            "   Cached: ${cacheCount}");
         try {
-            //TODO: Remove this
-            UIDialogs.toast("Tasks: ${tasks.filter { !it.fromCache }.size}\n" +
-                "Cached: ${tasks.filter { it.fromCache }.size}", false);
+            for(clientTasks in tasksGrouped) {
+                val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
+                val clientCacheCount = clientTasks.value.size - clientTaskCount;
+                if(clientCacheCount > 0) {
+                    UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels.")
+                }
+            }
+
         } catch (ex: Throwable){}
 
         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>>();
         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) {
             val forkTask = threadPool.submit<SubscriptionTaskResult> {
@@ -87,10 +153,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
                         pager = StatePlatform.instance.getChannelContent(task.client,
                             task.url, task.type, ResultCapabilities.ORDER_CHONOLOGICAL);
 
-                        pager = ChannelContentCache.cachePagerResults(scope, pager) {
-                            onNewCacheHit.emit(task.sub, it);
-                        };
-
                         val initialPage = pager.getResults();
                         task.sub.updateSubscriptionState(task.type, initialPage);
                         StateSubscriptions.instance.saveSubscription(task.sub);
@@ -105,6 +167,27 @@ abstract class SubscriptionsTaskFetchAlgorithm(
                     val channelEx = ChannelException(task.sub.channel, ex);
                     finished++;
                     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)
                         throw channelEx;
                     else {
@@ -117,39 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
             }
             forkTasks.add(forkTask);
         }
-
-        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);
+        return forkTasks;
     }
 
     abstract fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask>;
@@ -160,7 +211,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
         val sub: Subscription,
         val url: String,
         val type: String,
-        var fromCache: Boolean = false
+        var fromCache: Boolean = false,
+        var urgency: Int = 0
     );
 
     class SubscriptionTaskResult(
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
index f55742859558d30dc15e90fc605107e30973af22..b186afef76c3f1da3eb7c7aefac405b231bc6ae3 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
@@ -100,6 +100,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
     val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>();
     val onSourceEnded = Event0();
 
+    val onChapterChanged = Event2<IChapter?, Boolean>();
+
     val onVideoClicked = Event0();
     val onTimeBarChanged = Event2<Long, Long>();
 
@@ -185,6 +187,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
 
             override fun onScrubMove(timeBar: TimeBar, position: Long) {
                 gestureControl.restartHideJob();
+
+                updateCurrentChapter(position);
             }
 
             override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
@@ -233,17 +237,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
             val delta = position - lastPos;
             if(delta > 1000 || delta < 0) {
                 lastPos = position;
-                val currentChapter = getCurrentChapter(position)
-                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 = "";
-                    }
-                }
+                updateCurrentChapter();
             }
         }
 
@@ -256,6 +250,22 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
         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?) {
         if (drawable != null) {
             _videoView.defaultArtwork = drawable;
diff --git a/app/src/main/res/layout/big_button.xml b/app/src/main/res/layout/big_button.xml
index 8867d1f80941a8a02a37d1474d5e2a89e633ab5f..1cf84f1e17aaae0af0a5cacc9b4f765d1930b612 100644
--- a/app/src/main/res/layout/big_button.xml
+++ b/app/src/main/res/layout/big_button.xml
@@ -3,10 +3,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="60dp"
+    android:layout_height="wrap_content"
     android:gravity="center_vertical"
-    android:paddingTop="6dp"
-    android:paddingBottom="7dp"
     android:paddingStart="7dp"
     android:paddingEnd="12dp"
     android:background="@drawable/background_big_button"
@@ -26,6 +24,8 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
+        android:layout_marginTop="12dp"
+        android:layout_marginBottom="12dp"
         android:orientation="vertical">
         <TextView
             android:id="@+id/button_text"
@@ -47,7 +47,7 @@
             android:textSize="12dp"
             android:gravity="center_vertical"
             android:fontFamily="@font/inter_extra_light"
-            android:maxLines="1"
+            android:maxLines="2"
             android:ellipsize="end"
             tools:text="Attempts to fetch your subscriptions from this source" />
     </LinearLayout>
diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml
index c2a09caab1c3c693a7f1342e4c12168f38b570a3..be40a8645fec9e6106429bfb77c5806c8600f300 100644
--- a/app/src/main/res/layout/fragview_video_detail.xml
+++ b/app/src/main/res/layout/fragview_video_detail.xml
@@ -150,6 +150,30 @@
             android:textSize="12dp"
             android:fontFamily="@font/inter_light" />
     </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
         android:id="@+id/contentContainer"
diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube
index 5011bfcddb084007b938e6276b11f63e940006eb..ce7d9d0bc7cd18c4f50338e30a82ca132e24f70d 160000
--- a/app/src/unstable/assets/sources/youtube
+++ b/app/src/unstable/assets/sources/youtube
@@ -1 +1 @@
-Subproject commit 5011bfcddb084007b938e6276b11f63e940006eb
+Subproject commit ce7d9d0bc7cd18c4f50338e30a82ca132e24f70d