diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt
index 39944ee57b8a7c6bed635bfa0c6fda24e74469d1..71516d7f916a5399a501f279dc2028680c6c0ce1 100644
--- a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt
+++ b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt
@@ -1,15 +1,28 @@
-package com.futo.platformplayer
-
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DefaultHttpDataSource
+import androidx.media3.datasource.HttpDataSource
 import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
+import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
 import com.futo.platformplayer.helpers.VideoHelper
+import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
 
 fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
 fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
 fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
 
+@UnstableApi
+fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
+    val requestModifier = getRequestModifier();
+    return if (requestModifier != null) {
+        JSHttpDataSource.Factory().setRequestModifier(requestModifier);
+    } else {
+        DefaultHttpDataSource.Factory();
+    }
+}
 
 fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/PresetImages.kt b/app/src/main/java/com/futo/platformplayer/PresetImages.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cbdf2834f63f3b8e52194f63d9cc4267bbab4b2b
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/PresetImages.kt
@@ -0,0 +1,20 @@
+package com.futo.platformplayer
+
+class PresetImages {
+    companion object {
+        val images = mapOf<String, Int>(
+            Pair("xp_book", R.drawable.xp_book),
+            Pair("xp_forest", R.drawable.xp_forest),
+            Pair("xp_code", R.drawable.xp_code),
+            Pair("xp_controller", R.drawable.xp_controller),
+            Pair("xp_laptop", R.drawable.xp_laptop)
+        );
+
+        fun getPresetResIdByName(name: String): Int {
+            return images[name] ?: -1;
+        }
+        fun getPresetNameByResId(id: Int): String? {
+            return images.entries.find { it.value == id }?.key;
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt
index e0f6e96bab4cd325e7b7dbdf2c3cb839f6a1820d..6237f56c7f6c1d6065bc10e5e70566f8c2c38022 100644
--- a/app/src/main/java/com/futo/platformplayer/Settings.kt
+++ b/app/src/main/java/com/futo/platformplayer/Settings.kt
@@ -261,20 +261,23 @@ class Settings : FragmentedStorageFileJson() {
                 return FeedStyle.THUMBNAIL;
         }
 
-        @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
+        @FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
+        var showSubscriptionGroups: Boolean = true;
+
+        @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
         var previewFeedItems: Boolean = true;
 
-        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
+        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
         var progressBar: Boolean = true;
 
-        @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
+        @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
         @Serializable(with = FlexibleBooleanSerializer::class)
         var fetchOnAppBoot: Boolean = true;
 
-        @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 8)
+        @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
         var fetchOnTabOpen: Boolean = true;
 
-        @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 9)
+        @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10)
         @DropdownFieldOptionsId(R.array.background_interval)
         var subscriptionsBackgroundUpdateInterval: Int = 0;
 
@@ -290,7 +293,7 @@ class Settings : FragmentedStorageFileJson() {
         };
 
 
-        @FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 10)
+        @FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
         @DropdownFieldOptionsId(R.array.thread_count)
         var subscriptionConcurrency: Int = 3;
 
@@ -298,17 +301,17 @@ class Settings : FragmentedStorageFileJson() {
             return threadIndexToCount(subscriptionConcurrency);
         }
 
-        @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 11)
+        @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
         var showWatchMetrics: Boolean = false;
 
-        @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 12)
+        @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
         var allowPlaytimeTracking: Boolean = true;
 
 
-        @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 13)
+        @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
         var alwaysReloadFromCache: Boolean = false;
 
-        @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
+        @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
         fun clearChannelCache() {
             UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
             StateCache.instance.clear();
diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index f1a0ea440bcf5fbfd0c952a9d1c305f38c950af8..bac10f4a868c982dd6e2ec231dbb424e888738ea 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -3,8 +3,10 @@ package com.futo.platformplayer
 import android.content.ContentResolver
 import android.view.View
 import android.view.ViewGroup
+import com.futo.platformplayer.activities.MainActivity
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.models.ResultCapabilities
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
 import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
 import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
@@ -17,10 +19,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
 import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
 import com.futo.platformplayer.downloads.VideoLocal
+import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
 import com.futo.platformplayer.helpers.VideoHelper
 import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.ImageVariable
 import com.futo.platformplayer.models.Playlist
 import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
 import com.futo.platformplayer.parsers.HLS
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.states.StateDownloads
@@ -37,6 +42,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
 import com.futo.platformplayer.views.pills.RoundButton
 import com.futo.platformplayer.views.pills.RoundButtonGroup
 import com.futo.platformplayer.views.video.FutoVideoPlayerBase
+import isDownloadable
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -46,7 +52,7 @@ class UISlideOverlays {
     companion object {
         private const val TAG = "UISlideOverlays";
 
-        fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit,  vararg views: View) {
+        fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit,  vararg views: View): SlideUpMenuOverlay {
             var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
 
             menu.onOK.subscribe {
@@ -54,6 +60,7 @@ class UISlideOverlays {
                 onOk.invoke();
             };
             menu.show();
+            return menu;
         }
 
         fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
@@ -78,6 +85,7 @@ class UISlideOverlays {
                         SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
                             subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
                         }, false),
+
                         SlideUpMenuGroup(container.context, "Fetch Settings",
                             "Depending on the platform you might not need to enable a type for it to be available.",
                             -1, listOf()),
@@ -96,7 +104,15 @@ class UISlideOverlays {
                             }, false) else null,
                         if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
                             subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
-                        }, false) else null).filterNotNull());
+                        }, false) else null/*,,
+
+                        SlideUpMenuGroup(container.context, "Actions",
+                            "Various things you can do with this subscription",
+                            -1, listOf())
+                        SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
+                            showCreateSubscriptionGroup(container, subscription.channel);
+                        }, false)*/
+                        ).filterNotNull());
 
                     menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
 
@@ -134,6 +150,10 @@ class UISlideOverlays {
             }
         }
 
+        fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
+
+        }
+
         fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
             val items = arrayListOf<View>(LoaderView(container.context))
             val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@@ -512,6 +532,48 @@ class UISlideOverlays {
             return overlay;
         }
 
+        fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
+            val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
+            val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
+
+            addSubGroupOverlay.onOK.subscribe {
+                val text = nameInput.text;
+                if (text.isBlank()) {
+                    return@subscribe;
+                }
+
+                addSubGroupOverlay.hide();
+                nameInput.deactivate();
+                nameInput.clear();
+                if(onCreate == null)
+                {
+                    //TODO: Do this better, temp
+                    StateApp.instance.contextOrNull?.let {
+                        if(it is MainActivity) {
+                            val subGroup = SubscriptionGroup(text);
+                            if(initialChannel != null) {
+                                subGroup.urls.add(initialChannel.url);
+                                if(initialChannel.thumbnail != null)
+                                    subGroup.image = ImageVariable(initialChannel.thumbnail);
+                            }
+                            it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
+                        }
+                    }
+                }
+                else
+                    onCreate(text)
+            };
+
+            addSubGroupOverlay.onCancel.subscribe {
+                nameInput.deactivate();
+                nameInput.clear();
+            };
+
+            addSubGroupOverlay.show();
+            nameInput.activate();
+
+            return addSubGroupOverlay
+        }
         fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
             val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
             val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index 294e4fcf22c8b3b998b0b039044e192d5ff295b7..6dfe8472a73e9e79fa3db669140457eef2667c59 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -36,6 +36,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
 import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
 import com.futo.platformplayer.listeners.OrientationManager
 import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.SubscriptionGroup
 import com.futo.platformplayer.models.UrlVideoWithTime
 import com.futo.platformplayer.states.*
 import com.futo.platformplayer.stores.FragmentedStorage
@@ -100,6 +101,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
     lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
     lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
     lateinit var _fragBuy: BuyFragment;
+    lateinit var _fragSubGroup: SubscriptionGroupFragment;
+    lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
 
     lateinit var _fragBrowser: BrowserFragment;
 
@@ -235,6 +238,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         _fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
         _fragImportPlaylists = ImportPlaylistsFragment.newInstance();
         _fragBuy = BuyFragment.newInstance();
+        _fragSubGroup = SubscriptionGroupFragment.newInstance();
+        _fragSubGroupList = SubscriptionGroupListFragment.newInstance();
 
         _fragBrowser = BrowserFragment.newInstance();
 
@@ -316,6 +321,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         _fragDownloads.topBar = _fragTopBarGeneral;
         _fragImportSubscriptions.topBar = _fragTopBarImport;
         _fragImportPlaylists.topBar = _fragTopBarImport;
+        _fragSubGroup.topBar = _fragTopBarNavigation;
+        _fragSubGroupList.topBar = _fragTopBarAdd;
 
         _fragBrowser.topBar = _fragTopBarNavigation;
 
@@ -982,6 +989,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
             ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
             BrowserFragment::class -> _fragBrowser as T;
             BuyFragment::class -> _fragBuy as T;
+            SubscriptionGroupFragment::class -> _fragSubGroup as T;
+            SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
             else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
         }
     }
diff --git a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt
index d191f6421922722a0d6f38dcd2b726ea84276a99..ed42b7229f5f103f2ee29a917d03845d5c803527 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt
@@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
             Settings.instance.save()
         }
 
-        val items = Settings.instance.tabs.mapNotNull {
+        val items = ArrayList(Settings.instance.tabs.mapNotNull {
             val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
             TabViewHolderData(buttonDefinition, it.enabled)
-        };
+        });
 
         _listTabs = _recyclerTabs.asAny(items) {
             it.onDragDrop.subscribe { vh ->
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ea250ae450db058a52164ac4c77788356fcf028a
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt
@@ -0,0 +1,14 @@
+package com.futo.platformplayer.api.media.models.modifier
+
+class AdhocRequestModifier: IRequestModifier {
+    val _handler: (String, Map<String,String>)->IRequest;
+    override var allowByteSkip: Boolean = false;
+
+    constructor(modifyReq: (String, Map<String,String>)->IRequest) {
+        _handler = modifyReq;
+    }
+
+    override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
+        return _handler(url, headers);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1a7e4ce6b0933395be00b23224d6ae779f8608e4
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt
@@ -0,0 +1,6 @@
+package com.futo.platformplayer.api.media.models.modifier
+
+interface IModifierOptions {
+    val applyAuthClient: String?;
+    val applyCookieClient: String?;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..43cd502c1ba495e247e77bcd1e661117329d991c
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt
@@ -0,0 +1,6 @@
+package com.futo.platformplayer.api.media.models.modifier
+
+interface IRequest {
+    val url: String?;
+    val headers: Map<String, String>;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f15ee4771eba3fe9f6bd517d86b8a966a1acad31
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt
@@ -0,0 +1,7 @@
+package com.futo.platformplayer.api.media.models.modifier
+
+
+interface IRequestModifier {
+    var allowByteSkip: Boolean;
+    fun modifyRequest(url: String, headers: Map<String, String>): IRequest
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
index 1b929293260632f6fa6fdbd45336209b354f7129..47dfb1cb6c716f9de28b526186af88fd4adf1d7b 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
@@ -20,7 +20,7 @@ class DevJSClient : JSClient {
 
     val devID: String;
 
-    constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
+    constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null, settings: HashMap<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) {
         _devScript = script;
         _auth = auth;
         _captcha = captcha;
@@ -49,7 +49,7 @@ class DevJSClient : JSClient {
         _auth = auth;
     }
     fun recreate(context: Context): DevJSClient {
-        return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
+        return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
     }
 
     override fun getCopy(): JSClient {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
index f5f726864f689cc6d7aa4d096e5ccf2565b561f4..7106f02fbe78671dfc9eabf97861e013e324c8bc 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
@@ -7,6 +7,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
 import com.caoccao.javet.values.primitive.V8ValueString
 import com.caoccao.javet.values.reference.V8ValueArray
 import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.IPlatformClient
 import com.futo.platformplayer.api.media.PlatformClientCapabilities
 import com.futo.platformplayer.api.media.models.PlatformAuthorLink
@@ -67,8 +68,8 @@ open class JSClient : IPlatformClient {
     var descriptor: SourcePluginDescriptor
         private set;
 
-    private val _client: JSHttpClient;
-    private val _clientAuth: JSHttpClient?;
+    private val _httpClient: JSHttpClient;
+    private val _httpClientAuth: JSHttpClient?;
     private var _searchCapabilities: ResultCapabilities? = null;
     private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
     private var _channelCapabilities: ResultCapabilities? = null;
@@ -131,9 +132,9 @@ open class JSClient : IPlatformClient {
         _captcha = descriptor.getCaptchaData();
         flags = descriptor.flags.toTypedArray();
 
-        _client = JSHttpClient(this, null, _captcha);
-        _clientAuth = JSHttpClient(this, _auth, _captcha);
-        _plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
+        _httpClient = JSHttpClient(this, null, _captcha);
+        _httpClientAuth = JSHttpClient(this, _auth, _captcha);
+        _plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
         _plugin.withDependency(context, "scripts/polyfil.js");
         _plugin.withDependency(context, "scripts/source.js");
 
@@ -160,9 +161,9 @@ open class JSClient : IPlatformClient {
         _captcha = descriptor.getCaptchaData();
         flags = descriptor.flags.toTypedArray();
 
-        _client = JSHttpClient(this, null, _captcha);
-        _clientAuth = JSHttpClient(this, _auth, _captcha);
-        _plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
+        _httpClient = JSHttpClient(this, null, _captcha);
+        _httpClientAuth = JSHttpClient(this, _auth, _captcha);
+        _plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
         _plugin.withDependency(context, "scripts/polyfil.js");
         _plugin.withDependency(context, "scripts/source.js");
         _plugin.withScript(script);
@@ -181,6 +182,13 @@ open class JSClient : IPlatformClient {
     fun getUnderlyingPlugin(): V8Plugin {
         return _plugin;
     }
+    fun getHttpClientById(id: String): JSHttpClient? {
+        if(_httpClient.clientId == id)
+            return _httpClient;
+        if(_httpClientAuth?.clientId == id)
+            return _httpClientAuth;
+        return plugin.httpClientOthers[id];
+    }
 
     override fun initialize() {
         Logger.i(TAG, "Plugin [${config.name}] initializing");
@@ -254,7 +262,7 @@ open class JSClient : IPlatformClient {
     @JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
     override fun getHome(): IPager<IPlatformContent> = isBusyWith {
         ensureEnabled();
-        return@isBusyWith JSContentPager(config, plugin,
+        return@isBusyWith JSContentPager(config, this,
             plugin.executeTyped("source.getHome()"));
     }
 
@@ -292,7 +300,7 @@ open class JSClient : IPlatformClient {
     @JSDocsParameter("channelId", "(optional) Channel id to search in")
     override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
         ensureEnabled();
-        return@isBusyWith JSContentPager(config, plugin,
+        return@isBusyWith JSContentPager(config, this,
             plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
     }
 
@@ -316,7 +324,7 @@ open class JSClient : IPlatformClient {
         if(!capabilities.hasSearchChannelContents)
             throw IllegalStateException("This plugin does not support channel search");
 
-        return@isBusyWith JSContentPager(config, plugin,
+        return@isBusyWith JSContentPager(config, this,
             plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
     }
 
@@ -325,7 +333,7 @@ open class JSClient : IPlatformClient {
     @JSDocsParameter("query", "Query that channels should match")
     override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
         ensureEnabled();
-        return@isBusyWith JSChannelPager(config, plugin,
+        return@isBusyWith JSChannelPager(config, this,
             plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
     }
 
@@ -372,7 +380,7 @@ open class JSClient : IPlatformClient {
     @JSDocsParameter("filters", "(optional) Filters to apply on contents")
     override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
         ensureEnabled();
-        return@isBusyWith JSContentPager(config, plugin,
+        return@isBusyWith JSContentPager(config, this,
             plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
     }
 
@@ -438,7 +446,7 @@ open class JSClient : IPlatformClient {
     @JSDocsParameter("url", "A content url (this platform)")
     override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
         ensureEnabled();
-        return@isBusyWith IJSContentDetails.fromV8(config,
+        return@isBusyWith IJSContentDetails.fromV8(this,
             plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
     }
 
@@ -476,13 +484,13 @@ open class JSClient : IPlatformClient {
         if (pager !is V8ValueObject) { //TODO: Maybe solve this better
             return@isBusyWith EmptyPager<IPlatformComment>();
         }
-        return@isBusyWith JSCommentPager(config, plugin, pager);
+        return@isBusyWith JSCommentPager(config, this, pager);
     }
     @JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
     @JSDocsParameter("comment", "Comment object that was returned by getComments")
     override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
         ensureEnabled();
-        return comment.getReplies(this) ?: JSCommentPager(config, plugin,
+        return comment.getReplies(this) ?: JSCommentPager(config, this,
                 plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
     }
 
@@ -501,7 +509,7 @@ open class JSClient : IPlatformClient {
         if(!capabilities.hasGetLiveEvents)
             return@isBusyWith null;
         ensureEnabled();
-        return@isBusyWith JSLiveEventPager(config, plugin,
+        return@isBusyWith JSLiveEventPager(config, this,
             plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
     }
     @JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@@ -514,7 +522,7 @@ open class JSClient : IPlatformClient {
         ensureEnabled();
         if(!capabilities.hasSearchPlaylists)
             throw IllegalStateException("This plugin does not support playlist search");
-        return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
+        return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
     }
     @JSOptional
     @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@@ -530,7 +538,7 @@ open class JSClient : IPlatformClient {
     @JSDocsParameter("url", "Url of playlist")
     override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
         ensureEnabled();
-        return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
+        return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
     }
 
     @JSOptional
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
index 5833295e9df2e403e92d843796788acf37aaefce..e6a49777762d368f358d500a6d14fe17698b0c59 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
@@ -26,17 +26,19 @@ class SourcePluginDescriptor {
     @kotlinx.serialization.Transient
     val onCaptchaChanged = Event0();
 
-    constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
+    constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, settings: HashMap<String, String?>? = null) {
         this.config = config;
         this.authEncrypted = authEncrypted;
         this.captchaEncrypted = captchaEncrypted;
         this.flags = listOf();
+        this.settings = settings ?: hashMapOf();
     }
-    constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
+    constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>,  settings: HashMap<String, String?>? = null) {
         this.config = config;
         this.authEncrypted = authEncrypted;
         this.captchaEncrypted = captchaEncrypted;
         this.flags = flags;
+        this.settings = settings ?: hashMapOf();
     }
 
     fun getSettingsWithDefaults(): HashMap<String, String?> {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
index 05df143fcdcb50ab66af662c01093be9fe4ea467..8496dfc788b40a331c0eb7291d201c0c059585da 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
@@ -1,12 +1,16 @@
 package com.futo.platformplayer.api.media.platforms.js.internal
 
+import android.net.Uri
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.api.media.platforms.js.SourceAuth
 import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
+import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
 import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
 import com.futo.platformplayer.matchesDomain
+import java.util.UUID
 
 class JSHttpClient : ManagedHttpClient {
     private val _jsClient: JSClient?;
@@ -14,12 +18,15 @@ class JSHttpClient : ManagedHttpClient {
     private val _auth: SourceAuth?;
     private val _captcha: SourceCaptchaData?;
 
+    val clientId = UUID.randomUUID().toString();
+
     var doUpdateCookies: Boolean = true;
     var doApplyCookies: Boolean = true;
     var doAllowNewCookies: Boolean = true;
     val isLoggedIn: Boolean get() = _auth != null;
 
     private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
+    private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
 
     constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
         _jsClient = jsClient;
@@ -28,6 +35,7 @@ class JSHttpClient : ManagedHttpClient {
         _captcha = captcha;
 
         _currentCookieMap = hashMapOf();
+        _otherCookieMap = hashMapOf();
         if(!auth?.cookieMap.isNullOrEmpty()) {
             for(domainCookies in auth!!.cookieMap!!)
                 _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
@@ -49,6 +57,45 @@ class JSHttpClient : ManagedHttpClient {
         return newClient;
     }
 
+    //TODO: Use this in beforeRequest to remove dup code
+    fun applyHeaders(url: Uri, headers: MutableMap<String, String>, applyAuth: Boolean = false, applyOtherCookies: Boolean = false) {
+        val domain = url.host!!.lowercase();
+        val auth = _auth;
+        if (applyAuth && auth != null) {
+            //TODO: Possibly add doApplyHeaders
+            for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
+                headers.put(header.key, header.value);
+        }
+
+        if(doApplyCookies && (applyAuth || applyOtherCookies)) {
+            val cookiesToApply = hashMapOf<String, String>();
+            if(applyOtherCookies)
+                synchronized(_otherCookieMap) {
+                    for(cookie in _otherCookieMap
+                        .filter { domain.matchesDomain(it.key) }
+                        .flatMap { it.value.toList() })
+                        cookiesToApply[cookie.first] = cookie.second;
+                }
+            if(applyAuth)
+                synchronized(_currentCookieMap) {
+                    for(cookie in _currentCookieMap
+                        .filter { domain.matchesDomain(it.key) }
+                        .flatMap { it.value.toList() })
+                        cookiesToApply[cookie.first] = cookie.second;
+                };
+
+            if(cookiesToApply.size > 0) {
+                val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
+
+                val existingCookies = headers["Cookie"];
+                if(!existingCookies.isNullOrEmpty())
+                    headers.put("Cookie", existingCookies.trim(';') + "; " + cookieString);
+                else
+                    headers.put("Cookie", cookieString);
+            }
+        }
+    }
+
     override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
         val domain = request.url.host.lowercase();
         val auth = _auth;
@@ -101,10 +148,10 @@ class JSHttpClient : ManagedHttpClient {
             val defaultCookieDomain =
                 "." + domainParts.drop(domainParts.size - 2).joinToString(".");
             for (header in resp.headers) {
-                if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
+                if(header.first.lowercase() == "set-cookie") {
+                    var domainToUse = domain;
                     val cookie = cookieStringToPair(header.second);
                     var cookieValue = cookie.second;
-                    var domainToUse = domain;
 
                     if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
                         val cookieParts = cookie.second.split(";");
@@ -124,17 +171,33 @@ class JSHttpClient : ManagedHttpClient {
                         domainToUse = if (cookieVariables.containsKey("domain"))
                             cookieVariables["domain"]!!.lowercase();
                         else defaultCookieDomain;
+                        //TODO: Make sure this has no negative effect besides apply cookies to root domain
+                        if(!domainToUse.startsWith("."))
+                            domainToUse = ".${domainToUse}";
                     }
 
-                    val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
-                        _currentCookieMap[domainToUse]!!;
+                    if ((_auth != null || _currentCookieMap.isNotEmpty())) {
+                        val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
+                            _currentCookieMap[domainToUse]!!;
+                        else {
+                            val newMap = hashMapOf<String, String>();
+                            _currentCookieMap[domainToUse] = newMap
+                            newMap;
+                        }
+                        if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
+                            cookieMap[cookie.first] = cookieValue;
+                    }
                     else {
-                        val newMap = hashMapOf<String, String>();
-                        _currentCookieMap[domainToUse] = newMap
-                        newMap;
+                        val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
+                            _otherCookieMap[domainToUse]!!;
+                        else {
+                            val newMap = hashMapOf<String, String>();
+                            _otherCookieMap[domainToUse] = newMap
+                            newMap;
+                        }
+                        if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
+                            cookieMap[cookie.first] = cookieValue;
                     }
-                    if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
-                        cookieMap[cookie.first] = cookieValue;
                 }
             }
         }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
index 09da4562761b5c9d6abe5352ac28c117954335c1..a6a15fb63817667c2a78496e6b8c13f38100d3ba 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.contents.ContentType
 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.getOrDefault
 import com.futo.platformplayer.getOrThrow
@@ -10,13 +11,14 @@ import com.futo.platformplayer.getOrThrow
 interface IJSContent: IPlatformContent  {
 
     companion object {
-        fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContent {
+        fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
+            val config = plugin.config;
             val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
             val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
 
             //TODO: Temporary workaround for intercepting details in lists
             if(pluginType != null && pluginType.endsWith("Details"))
-                return IJSContentDetails.fromV8(config, obj);
+                return IJSContentDetails.fromV8(plugin, obj);
 
             return when(ContentType.fromInt(type)) {
                 ContentType.MEDIA -> JSVideo(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
index fad34868de7d3aae94889f5604acea08c7f2fbae..6ec2fd98204e92c63e39c6734bb76bbb45009993 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
@@ -4,17 +4,18 @@ import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.contents.ContentType
 import com.futo.platformplayer.api.media.models.contents.IPlatformContent
 import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
 import com.futo.platformplayer.getOrThrow
 
 interface IJSContentDetails: IPlatformContent  {
 
     companion object {
-        fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContentDetails {
-            val type: Int = obj.getOrThrow(config, "contentType", "ContentDetails");
+        fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
+            val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
             return when(ContentType.fromInt(type)) {
-                ContentType.MEDIA -> JSVideoDetails(config, obj);
-                ContentType.POST -> JSPostDetails(config, obj);
+                ContentType.MEDIA -> JSVideoDetails(plugin, obj);
+                ContentType.POST -> JSPostDetails(plugin.config, obj);
                 else -> throw NotImplementedError("Unknown content type ${type}");
             }
         }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt
index 012538961e469b1a92f84a3ac727eac3b4813583..683c64afde8371f06e2d0a5a33fde148fa506c6e 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt
@@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.PlatformAuthorLink
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
 
 class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
 
     override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
         return PlatformAuthorLink.fromV8(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt
index 04146582388801e1cef8650491289638b09c04a8..ab847b6bf229c24cfc1387c4fa839aa56c8687fc 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
 import com.futo.platformplayer.api.media.models.PlatformAuthorLink
 import com.futo.platformplayer.api.media.models.comments.IPlatformComment
 import com.futo.platformplayer.api.media.models.ratings.IRating
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
@@ -60,6 +61,7 @@ class JSComment : IPlatformComment {
             return null;
 
         val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
-        return JSCommentPager(_config!!, _plugin!!, obj);
+        val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
+        return JSCommentPager(_config!!, plugin, obj);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt
index 13d44fe9e2ead5f3c13a00e84c61fe1ddf21c933..94205d35168a70d24eac48254090c794e4b24af7 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt
@@ -2,15 +2,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.comments.IPlatformComment
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
 
 class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> {
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { }
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { }
 
     override fun convertResult(obj: V8ValueObject): IPlatformComment {
-        return JSComment(config, plugin, obj);
+        return JSComment(config, plugin.getUnderlyingPlugin(), obj);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt
index f7a8fbbc304270978846a72309074a6823a9f50d..490fa7c4df55160aab2f4babea25132c718cde40 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt
@@ -3,15 +3,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.IPluginSourced
 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.engine.V8Plugin
 
 class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
     override val sourceConfig: SourcePluginConfig get() = config;
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
 
     override fun convertResult(obj: V8ValueObject): IPlatformContent {
-        return IJSContent.fromV8(config, obj);
+        return IJSContent.fromV8(plugin, obj);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
index 1d8ead850f3f126d3780b313aab4a2d58849042c..27731feac4435356910d055b1d3f7877f1b51059 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
+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.IPlatformLiveEventPager
 import com.futo.platformplayer.engine.V8Plugin
@@ -10,7 +11,7 @@ import com.futo.platformplayer.getOrThrow
 class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
     override var nextRequest: Int;
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
         nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
index 831cc93d0120400cce59a2a35f44460205bc8329..a767282310850854d172a6042679335bd9fc9ac0 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
@@ -4,6 +4,7 @@ import android.os.Looper
 import com.caoccao.javet.values.reference.V8ValueArray
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.BuildConfig
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
@@ -12,7 +13,7 @@ import com.futo.platformplayer.getOrThrow
 import com.futo.platformplayer.warnIfMainThread
 
 abstract class JSPager<T> : IPager<T> {
-    protected val plugin: V8Plugin;
+    protected val plugin: JSClient;
     protected val config: SourcePluginConfig;
     protected var pager: V8ValueObject;
 
@@ -21,9 +22,9 @@ abstract class JSPager<T> : IPager<T> {
     private var _hasMorePages: Boolean = false;
     //private var _morePagesWasFalse: Boolean = false;
 
-    val isAvailable get() = plugin._runtime?.let { !it.isClosed && !it.isDead } ?: false;
+    val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) {
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) {
         this.plugin = plugin;
         this.pager = pager;
         this.config = config;
@@ -43,7 +44,7 @@ abstract class JSPager<T> : IPager<T> {
     override fun nextPage() {
         warnIfMainThread("JSPager.nextPage");
 
-        pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
+        pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
             pager.invoke("nextPage", arrayOf<Any>());
         };
         _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
index 97b3f29b0fbc32ae4292434e61dd96339c3d78c2..cbd4e0138c7499023d3099a824cc26f4077776c4 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
 import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
@@ -13,7 +14,7 @@ import com.futo.platformplayer.models.Playlist
 class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
     override val contents: IPager<IPlatformVideo>;
 
-    constructor(plugin: V8Plugin, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
+    constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
         contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt
index a0df057b0a5fa2a4f129c273f3e3d4a4da9b2aae..151bfe2afd8a8840d102a56f340a77e7c2ea66f7 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt
@@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
+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.IPager
 import com.futo.platformplayer.engine.V8Plugin
 
 class JSPlaylistPager : JSPager<IPlatformPlaylist>, IPager<IPlatformPlaylist> {
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
 
     override fun convertResult(obj: V8ValueObject): IPlatformPlaylist {
         return JSPlaylist(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
index e898288946fbaeeff2da8476be0e77ce898ad284..bc455fcc1d1a33c13fe5625af33c4299c91bb54b 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
@@ -54,6 +54,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
 
     private fun getCommentsJS(client: JSClient): JSCommentPager {
         val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
-        return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
+        return JSCommentPager(_pluginConfig, client, commentPager);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt
index 960982bf1018a07fd826d55efd2ffd9cea128bf4..1a9ef32b4fd0218e8bde298ff41b13b9ab0542f5 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt
@@ -1,18 +1,81 @@
 package com.futo.platformplayer.api.media.platforms.js.models
 
+import android.net.Uri
 import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.api.media.models.modifier.IModifierOptions
+import com.futo.platformplayer.api.media.models.modifier.IRequest
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
-import com.futo.platformplayer.getOrThrow
-import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.getOrDefault
 
 @kotlinx.serialization.Serializable
-class JSRequest : JSRequestModifier.IRequest {
-    override val url: String;
-    override val headers: Map<String, String>;
+class JSRequest : IRequest {
+    private val _v8Url: String?;
+    private val _v8Headers: Map<String, String>?;
+    private val _v8Options: Options?;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) {
+    override var url: String? = null;
+    override lateinit var headers: Map<String, String>;
+
+    constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
+        _v8Url = url;
+        _v8Headers = headers;
+        _v8Options = options;
+        initialize(plugin, originalUrl, originalHeaders);
+    }
+    constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) {
         val contextName = "ModifyRequestResponse";
-        url = obj.getOrThrow(config, "url", contextName);
-        headers = obj.getOrThrow(config, "headers", contextName);
+        val config = plugin.config;
+        _v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
+        _v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
+        _v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
+            Options(config, it);
+        }
+        initialize(plugin, originalUrl, originalHeaders);
+    }
+
+    private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
+        val config = plugin.config;
+        url = _v8Url ?: originalUrl;
+        headers = _v8Headers ?: originalHeaders ?: mapOf();
+
+        if(_v8Options != null) {
+            if(_v8Options.applyCookieClient != null && url != null) {
+                val client = plugin.getHttpClientById(_v8Options.applyCookieClient);
+                if(client != null) {
+                    val toModifyHeaders = headers.toMutableMap();
+                    client.applyHeaders(Uri.parse(url), toModifyHeaders, false, true);
+                    headers = toModifyHeaders;
+                }
+            }
+            if(_v8Options.applyAuthClient != null && url != null) {
+                val client = plugin.getHttpClientById(_v8Options.applyAuthClient);
+                if(client != null) {
+                    val toModifyHeaders = headers.toMutableMap();
+                    client.applyHeaders(Uri.parse(url), toModifyHeaders, true, false);
+                    headers = toModifyHeaders;
+                }
+            }
+        }
+    }
+
+    fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
+        return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
+    }
+
+
+    @kotlinx.serialization.Serializable
+    class Options: IModifierOptions {
+        override val applyAuthClient: String?;
+        override val applyCookieClient: String?;
+
+        constructor(config: IV8PluginConfig, obj: V8ValueObject) {
+            applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
+            applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
+        }
+    }
+
+    companion object {
+
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
index 0d71057a093c6faee7252d89d45cf302113913cf..cffbb4743aea52a9e288430113e2fc3bc26b0c87 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
@@ -1,19 +1,28 @@
 package com.futo.platformplayer.api.media.platforms.js.models
 
+import android.net.Uri
 import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.api.media.models.modifier.IRequest
+import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
 import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
+import com.futo.platformplayer.getOrDefault
 import com.futo.platformplayer.getOrNull
+import com.futo.platformplayer.getOrThrow
 
-class JSRequestModifier {
+class JSRequestModifier: IRequestModifier {
+    private val _plugin: JSClient;
     private val _config: IV8PluginConfig;
     private var _modifier: V8ValueObject;
-    val allowByteSkip: Boolean;
+    override var allowByteSkip: Boolean;
 
-    constructor(config: IV8PluginConfig, modifier: V8ValueObject) {
+    constructor(plugin: JSClient, modifier: V8ValueObject) {
+        this._plugin = plugin;
         this._modifier = modifier;
-        this._config = config;
+        this._config = plugin.config;
+        val config = plugin.config;
 
         allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
 
@@ -21,22 +30,19 @@ class JSRequestModifier {
             throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
     }
 
-    fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
+    override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
         if (_modifier.isClosed) {
             return Request(url, headers);
         }
 
         val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
             _modifier.invoke("modifyRequest", url, headers);
-        };
+        } as V8ValueObject;
 
-        return JSRequest(_config, result as V8ValueObject);
+        val req = JSRequest(_plugin, result, url, headers);
+        return req;
     }
 
-    interface IRequest {
-        val url: String;
-        val headers: Map<String, String>;
-    }
 
     data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
index 815452ee6b5431da53d7e5ff2d9e9e16a7bde540..83137f23ecd06cf5493ea58e4e2e426e8a9710a9 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
@@ -44,13 +44,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
     override val subtitles: List<ISubtitleSource>;
 
 
-    constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
         val contextName = "VideoDetails";
+        val config = plugin.config;
         description = _content.getOrThrow(config, "description", contextName);
-        video = JSVideoSourceDescriptor.fromV8(config, _content.getOrThrow(config, "video", contextName));
-        dash =  JSSource.fromV8DashNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
-        hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
-        live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
+        video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
+        dash =  JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
+        hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
+        live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
         rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
 
         if(!_content.has("subtitles"))
@@ -105,6 +106,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
         if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
             return null;
 
-        return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
+        return JSCommentPager(_pluginConfig, client, commentPager);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt
index 1cc24a2d2dbf802e80970895f43f11ddd099dd14..89117138cd91ccb3a0e23af4a4cd1bb8ebaf6d03 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt
@@ -2,12 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js.models
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
 import com.futo.platformplayer.engine.V8Plugin
 
 class JSVideoPager : JSPager<IPlatformVideo> {
 
-    constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
+    constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
 
     override fun convertResult(obj: V8ValueObject): IPlatformVideo {
         return JSVideo(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt
index 39d800323f6a1e5d8875655b00693be00b0772cf..09de1f35674d04fa7eb457a1f28bf191b87b70ce 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt
@@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrDefault
 import com.futo.platformplayer.getOrThrow
 
@@ -19,8 +21,9 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
 
     override var priority: Boolean = false;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_AUDIOURL, config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
         val contextName = "AudioUrlSource";
+        val config = plugin.config;
 
         bitrate = _obj.getOrThrow(config, "bitrate", contextName);
         container = _obj.getOrThrow(config, "container", contextName);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt
index 9eafafeeed83ec729e272f23a339a7cbf4cbf962..42eeffce95b3472707ccda96875d09fa76747fd2 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
 import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrDefault
 
 class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
@@ -22,8 +24,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
         && indexEnd != null)
         StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
         val contextName = "JSAudioUrlRangeSource";
+        val config = plugin.config;
 
         itagId = _obj.getOrDefault(config, "itagId", contextName, null);
         initStart = _obj.getOrDefault(config, "initStart", contextName, null);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt
index 4f50cbf685c5c86cf5ab3e92a41dd1ad0a3777a7..3070a2d4e26ed1df1a006f9b531225497352abc3 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrNull
 import com.futo.platformplayer.getOrThrow
 
@@ -19,9 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
 
     override var priority: Boolean = false;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_DASH, config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
         val contextName = "DashSource";
-
+        val config = plugin.config;
         name = _obj.getOrThrow(config, "name", contextName);
         url = _obj.getOrThrow(config, "url", contextName);
         duration = _obj.getOrThrow(config, "duration", contextName);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
index 8636cbb810706884d95017ae905bdec86bc25d3b..4194880220a3b11c2c903b6328ed8e16172b85d5 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
@@ -4,7 +4,9 @@ import com.caoccao.javet.values.V8Value
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
 import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrNull
 import com.futo.platformplayer.getOrThrow
 import com.futo.platformplayer.orNull
@@ -20,8 +22,9 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
 
     override var priority: Boolean = false;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
         val contextName = "HLSAudioSource";
+        val config = plugin.config;
 
         name = _obj.getOrThrow(config, "name", contextName);
         url = _obj.getOrThrow(config, "url", contextName);
@@ -33,7 +36,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
 
 
     companion object {
-        fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
-        fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(config, obj);
+        fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
+        fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt
index 8c785c1756ba3f51dcb9f96779625ba8b8164ab3..606d107c3811211528ad083c4dad91a99f165025 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrNull
 import com.futo.platformplayer.getOrThrow
 
@@ -19,8 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
 
     override var priority: Boolean = false;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
         val contextName = "HLSSource";
+        val config = plugin.config;
 
         name = _obj.getOrThrow(config, "name", contextName);
         url = _obj.getOrThrow(config, "url", contextName);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
index 9bf35ad204819d5056caa4a33860de75b51f8f55..ccd9c22386b14248fca3ed359daa4d9ff7f7b884 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
@@ -4,31 +4,47 @@ import androidx.media3.datasource.DefaultHttpDataSource
 import androidx.media3.datasource.HttpDataSource
 import com.caoccao.javet.values.V8Value
 import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
+import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
+import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
 import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
 import com.futo.platformplayer.engine.IV8PluginConfig
 import com.futo.platformplayer.engine.V8Plugin
+import com.futo.platformplayer.getOrDefault
 import com.futo.platformplayer.orNull
 import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
 
 abstract class JSSource {
+    protected val _plugin: JSClient;
     protected val _config: IV8PluginConfig;
     protected val _obj: V8ValueObject;
-    private val _hasRequestModifier: Boolean;
+    val hasRequestModifier: Boolean;
+    private val _requestModifier: JSRequest?;
 
     val type : String;
 
-    constructor(type: String, config: IV8PluginConfig, obj: V8ValueObject) {
-        this._config = config;
+    constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
+        this._plugin = plugin;
+        this._config = plugin.config;
         this._obj = obj;
         this.type = type;
 
-        _hasRequestModifier = obj.has("getRequestModifier");
+        _requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
+            JSRequest(plugin, it, null, null);
+        }
+        hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
     }
 
-    fun getRequestModifier(): JSRequestModifier? {
-        if (!_hasRequestModifier || _obj.isClosed) {
+    fun getRequestModifier(): IRequestModifier? {
+        if(_requestModifier != null)
+            return AdhocRequestModifier { url, headers ->
+                  return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
+            };
+
+        if (!hasRequestModifier || _obj.isClosed) {
             return null;
         }
 
@@ -40,16 +56,7 @@ abstract class JSSource {
             return null;
         }
 
-        return JSRequestModifier(_config, result)
-    }
-
-    fun getHttpDataSourceFactory(): HttpDataSource.Factory {
-        val requestModifier = getRequestModifier();
-        return if (requestModifier != null) {
-            JSHttpDataSource.Factory().setRequestModifier(requestModifier);
-        } else {
-            DefaultHttpDataSource.Factory();
-        }
+        return JSRequestModifier(_plugin, result)
     }
 
     companion object {
@@ -60,28 +67,28 @@ abstract class JSSource {
         const val TYPE_DASH = "DashSource";
         const val TYPE_HLS = "HLSSource";
 
-        fun fromV8VideoNullable(config: IV8PluginConfig, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(config, it as V8ValueObject) };
-        fun fromV8Video(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSource {
+        fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
+        fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
             val type = obj.getString("plugin_type");
             return when(type) {
-                TYPE_VIDEOURL -> JSVideoUrlSource(config, obj);
-                TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(config, obj);
-                TYPE_HLS -> fromV8HLS(config, obj);
-                TYPE_DASH -> fromV8Dash(config, obj);
+                TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
+                TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
+                TYPE_HLS -> fromV8HLS(plugin, obj);
+                TYPE_DASH -> fromV8Dash(plugin, obj);
                 else -> throw NotImplementedError("Unknown type ${type}");
             }
         }
-        fun fromV8DashNullable(config: IV8PluginConfig, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(config, it as V8ValueObject) };
-        fun fromV8Dash(config: IV8PluginConfig, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(config, obj);
-        fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
-        fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(config, obj);
+        fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
+        fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
+        fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
+        fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
 
-        fun fromV8Audio(config: IV8PluginConfig, obj: V8ValueObject) : IAudioSource {
+        fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
             val type = obj.getString("plugin_type");
             return when(type) {
-                TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(config, obj);
-                TYPE_AUDIOURL -> JSAudioUrlSource(config, obj);
-                TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(config, obj);
+                TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
+                TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
+                TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
                 else -> throw NotImplementedError("Unknown type ${type}");
             }
         }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt
index 08d4911d7ec395e33b2cfde2c9a50740a2c1857d..035f5fb64d13699aaf25335088981a3bf36964f0 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
 import com.futo.platformplayer.getOrThrow
 
@@ -15,15 +16,16 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
     override val videoSources: Array<IVideoSource>;
     override val audioSources: Array<IAudioSource>;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) {
+    constructor(plugin: JSClient, obj: V8ValueObject) {
         this._obj = obj;
+        val config = plugin.config;
         val contextName = "UnMuxVideoSource"
         this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
         this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
-            .map { JSSource.fromV8Video(config, it as V8ValueObject) }
+            .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
             .toTypedArray();
         this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
-            .map { JSSource.fromV8Audio(config, it as V8ValueObject) }
+            .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
             .toTypedArray();
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
index 463100b05f02213fd53245e4be4aafc6ef5787a6..131a5794bf81578f58ba04981ae040ffb931f21f 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
@@ -5,7 +5,9 @@ import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrThrow
 
 class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
@@ -14,12 +16,13 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
     override val isUnMuxed: Boolean;
     override val videoSources: Array<IVideoSource>;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) {
+    constructor(plugin: JSClient, obj: V8ValueObject) {
         this._obj = obj;
+        val config = plugin.config;
         val contextName = "VideoSourceDescriptor";
         this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
         this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
-            .map { JSSource.fromV8Video(config, it as V8ValueObject) }
+            .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
             .toTypedArray();
     }
 
@@ -28,11 +31,11 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
         const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor";
 
 
-        fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSourceDescriptor {
+        fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
             val type = obj.getString("plugin_type")
             return when(type) {
-                TYPE_MUXED -> JSVideoSourceDescriptor(config, obj);
-                TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(config, obj);
+                TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
+                TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(plugin, obj);
                 else -> throw NotImplementedError("Unknown type: ${type}");
             }
         }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt
index a4fbf0e80e02f53866d9e22f26329320aa640bad..66246f56fec6db378e8da289a2744407ee8cef72 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt
@@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrNull
 import com.futo.platformplayer.getOrThrow
 
@@ -18,8 +20,9 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
 
     override var priority: Boolean = false;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject): super(TYPE_VIDEOURL, config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
         val contextName = "JSVideoUrlSource";
+        val config = plugin.config;
 
         width = _obj.getOrThrow(config, "width", contextName);
         height = _obj.getOrThrow(config, "height", contextName);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt
index 90b6edee7de886d12ccc94f42adcefbb4abc6acb..0e86c2fca0697ff05fec7033dd9907378758a012 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt
@@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
 import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
+import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.getOrDefault
 
 class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
@@ -21,8 +23,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
         && indexEnd != null)
             StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
 
-    constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) {
+    constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
         val contextName = "JSVideoUrlRangeSource";
+        val config = plugin.config;
 
         itagId = _obj.getOrDefault(config, "itagId", contextName, null);
         initStart = _obj.getOrDefault(config, "initStart", contextName, null);
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
index fe1dad584e3a69db63b4bdca7dfa1a78af2324ab..9dadb54e246d211f09fe77dacfa35316f8e6d436 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
@@ -27,10 +27,8 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
 import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.exceptions.DownloadException
-import com.futo.platformplayer.hasAnySource
 import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
 import com.futo.platformplayer.helpers.VideoHelper
-import com.futo.platformplayer.isDownloadable
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.parsers.HLS
 import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
@@ -38,6 +36,8 @@ import com.futo.platformplayer.states.StateDownloads
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.toHumanBitrate
 import com.futo.platformplayer.toHumanBytesSpeed
+import hasAnySource
+import isDownloadable
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
index 95ad1b3a0b77a526af57318562b09fcf77adddee..8b7c08da51b4059dd8d3f71d53760121687b27fe 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
@@ -11,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
 import com.caoccao.javet.values.primitive.V8ValueString
 import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.api.http.ManagedHttpClient
+import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.engine.exceptions.NoInternetException
 import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
@@ -34,15 +35,24 @@ import com.futo.platformplayer.getOrThrow
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateAssets
 import com.futo.platformplayer.warnIfMainThread
+import java.util.concurrent.ConcurrentHashMap
 
 class V8Plugin {
     val config: IV8PluginConfig;
     private val _client: ManagedHttpClient;
     private val _clientAuth: ManagedHttpClient;
+    private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
 
 
     val httpClient: ManagedHttpClient get() = _client;
     val httpClientAuth: ManagedHttpClient get() = _clientAuth;
+    val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
+
+    fun registerHttpClient(client: JSHttpClient) {
+        synchronized(_clientOthers) {
+            _clientOthers.put(client.clientId, client);
+        }
+    }
 
     private val _runtimeLock = Object();
     var _runtime : V8Runtime? = null;
diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
index 68a742027d3a964610b0c38674bf78ca882bfbde..937e2d23bcb05272e4c9b71886f38fa7bdd54eab 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
@@ -45,7 +45,12 @@ class PackageHttp: V8Package {
 
     @V8Function
     fun newClient(withAuth: Boolean): PackageHttpClient {
-        return PackageHttpClient(this, if(withAuth) _clientAuth.clone() else _client.clone());
+        val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
+        if(httpClient is JSHttpClient)
+            _plugin.registerHttpClient(httpClient);
+        val client = PackageHttpClient(this, httpClient);
+
+        return client;
     }
     @V8Function
     fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
@@ -187,10 +192,19 @@ class PackageHttp: V8Package {
 
         @Transient
         private val _defaultHeaders = mutableMapOf<String, String>();
+        @Transient
+        private val _clientId: String?;
+
+        @V8Property
+        fun clientId(): String? {
+            return _clientId;
+        }
+
 
         constructor(pack: PackageHttp, baseClient: ManagedHttpClient): super() {
             _package = pack;
             _client = baseClient;
+            _clientId = if(_client is JSHttpClient) _client.clientId else null;
         }
 
         @V8Function
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
index 48f685902ce8eb110b1065f714f3f8fa117216c7..979a679add16396015fa90f159d875ec563e912f 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
@@ -348,6 +348,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
             ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
             ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
             ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
+            ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
             ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
                 val c = it.context ?: return@ButtonDefinition;
                 Logger.i(TAG, "settings preventPictureInPicture()");
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 a28e8016cf8196edef8fd4513e7230c6a7c95f47..3af3f01d8241678a7466ded0850616df48602b86 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
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateDeveloper
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.states.StatePlugins
 import com.futo.platformplayer.views.buttons.BigButton
@@ -294,7 +295,9 @@ class SourceDetailFragment : MainFragment() {
                 }
             }
 
-            val clientIfExists = StatePlugins.instance.getPlugin(config.id);
+            val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
+                StatePlugins.instance.getPlugin(config.id);
+            else null;
             groups.add(
                 BigButtonGroup(c, context.getString(R.string.management),
                     BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f77df2cd8fed1994582f5d1e718ca9c873f3dfd3
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt
@@ -0,0 +1,302 @@
+package com.futo.platformplayer.fragment.mainactivity.main
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Color
+import android.os.Bundle
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.getSystemService
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UISlideOverlays
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.states.StateSubscriptionGroups
+import com.futo.platformplayer.states.StateSubscriptions
+import com.futo.platformplayer.views.AnyAdapterView
+import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
+import com.futo.platformplayer.views.SearchView
+import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
+import com.futo.platformplayer.views.overlays.ImageVariableOverlay
+import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
+import com.google.android.material.imageview.ShapeableImageView
+import com.google.android.material.shape.CornerFamily
+import com.google.android.material.shape.ShapeAppearanceModel
+
+class SubscriptionGroupFragment : MainFragment() {
+    override val isMainView : Boolean = true;
+    override val isTab: Boolean = false;
+    override val hasBottomBar: Boolean get() = true;
+
+    private var _view: SubscriptionGroupView? = null;
+
+    override fun onShownWithView(parameter: Any?, isBack: Boolean) {
+        super.onShownWithView(parameter, isBack);
+
+        if(parameter is SubscriptionGroup)
+            _view?.setGroup(StateSubscriptionGroups.instance.getSubscriptionGroup(parameter.id) ?: parameter);
+        else
+            _view?.setGroup(null);
+    }
+
+    override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        val view = SubscriptionGroupView(requireContext(), this);
+        _view = view;
+        return view;
+    }
+
+    companion object {
+        private const val TAG = "SourcesFragment";
+        fun newInstance() = SubscriptionGroupFragment().apply {}
+    }
+
+
+    private class SubscriptionGroupView: ConstraintLayout {
+        private val _fragment: SubscriptionGroupFragment;
+
+        private val _textGroupTitleContainer: LinearLayout;
+        private val _textGroupTitle: TextView;
+        private val _imageGroup: ShapeableImageView;
+        private val _imageGroupBackground: ImageView;
+        private val _buttonEditImage: LinearLayout;
+        private val _searchBar: SearchView;
+
+        private val _textGroupMeta: TextView;
+
+        private val _buttonSettings: ImageButton;
+        private val _buttonDelete: ImageButton;
+
+        private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
+        private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
+        private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
+        private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
+
+        private val _containerEnabled: LinearLayout;
+        private val _containerDisabled: LinearLayout;
+
+        private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
+        private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
+
+        private val _overlay: FrameLayout;
+
+        private var _group: SubscriptionGroup? = null;
+
+        constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
+            inflate(context, R.layout.fragment_subscriptions_group, this);
+            _fragment = fragment;
+
+            _overlay = findViewById(R.id.overlay);
+            _searchBar = findViewById(R.id.search_bar);
+            _textGroupTitleContainer = findViewById(R.id.text_group_title_container);
+            _textGroupTitle = findViewById(R.id.text_group_title);
+            _imageGroup = findViewById(R.id.image_group);
+            _imageGroupBackground = findViewById(R.id.group_image_background);
+            _buttonEditImage = findViewById(R.id.button_edit_image);
+            _textGroupMeta = findViewById(R.id.text_group_meta);
+            _buttonSettings = findViewById(R.id.button_settings);
+            _buttonDelete = findViewById(R.id.button_delete);
+            _imageGroup.setBackgroundColor(Color.GRAY);
+
+            val dp6 = 6.dp(resources);
+            _imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
+                .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
+                .build()
+
+            _containerEnabled = findViewById(R.id.container_enabled);
+            _containerDisabled = findViewById(R.id.container_disabled);
+            _recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
+                it.itemView.setPadding(0, dp6, 0, dp6);
+                it.onClick.subscribe { channel ->
+                    disableCreator(channel);
+                };
+            }
+            _recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
+                it.itemView.setPadding(0, dp6, 0, dp6);
+                it.onClick.subscribe { channel ->
+                    enableCreator(channel);
+                };
+            }
+            _recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
+                this.orientation = LinearLayoutManager.VERTICAL;
+            };
+            _recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
+                this.orientation = LinearLayoutManager.VERTICAL;
+            };
+
+            _textGroupTitleContainer.setOnClickListener {
+                _group?.let { editName(it) };
+            };
+            _textGroupMeta.setOnClickListener {
+                _group?.let { editName(it) };
+            };
+            _imageGroup.setOnClickListener {
+                _group?.let { editImage(it) };
+            };
+            _buttonEditImage.setOnClickListener {
+                _group?.let { editImage(it) }
+            };
+            _buttonSettings.setOnClickListener {
+
+            }
+            _buttonDelete.setOnClickListener {
+                _group?.let {
+                    StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id);
+                };
+                fragment.close(true);
+            }
+            _buttonSettings.visibility = View.GONE;
+
+            _searchBar.onSearchChanged.subscribe {
+                filterCreators();
+            }
+
+            setGroup(null);
+        }
+
+        fun save() {
+            _group?.let {
+                StateSubscriptionGroups.instance.updateSubscriptionGroup(it);
+            };
+        }
+
+        fun editName(group: SubscriptionGroup) {
+            val editView = SlideUpMenuTextInput(context, "Group name");
+            editView.text = group.name;
+            UISlideOverlays.showOverlay(_overlay, "Edit name", "Save", {
+                editView.deactivate();
+                val text = editView.text;
+                if(!text.isNullOrEmpty()) {
+                    group.name = text;
+                    _textGroupTitle.text = text;
+                    save();
+                }
+            }, editView).onCancel.subscribe {
+                editView.deactivate();
+            }
+            editView.activate();
+        }
+        fun editImage(group: SubscriptionGroup) {
+            val overlay = ImageVariableOverlay(context, _enabledCreators.map { it.url });
+            _overlay.removeAllViews();
+            _overlay.addView(overlay);
+            _overlay.alpha = 0f
+            _overlay.visibility = View.VISIBLE;
+            _overlay.animate().alpha(1f).setDuration(300).start();
+            overlay.onSelected.subscribe {
+                group.image = it;
+                it.setImageView(_imageGroup);
+                it.setImageView(_imageGroupBackground);
+                save();
+            };
+            overlay.onClose.subscribe {
+                _overlay.visibility = View.GONE;
+                overlay.removeAllViews();
+            }
+        }
+
+
+        fun setGroup(group: SubscriptionGroup?) {
+            _group = group;
+            _textGroupTitle.text = group?.name;
+
+            val image = group?.image;
+            if(image != null) {
+                image.setImageView(_imageGroupBackground);
+                image.setImageView(_imageGroup);
+            }
+            else {
+                _imageGroupBackground.setImageResource(0);
+                _imageGroup.setImageResource(0);
+            }
+            updateMeta();
+            reloadCreators(group);
+        }
+
+        @SuppressLint("NotifyDataSetChanged")
+        private fun reloadCreators(group: SubscriptionGroup?) {
+            _enabledCreators.clear();
+            _disabledCreators.clear();
+
+            if(group != null) {
+                val urls = group.urls.toList();
+                val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
+                _enabledCreators.addAll(subs.filter { urls.contains(it.url) });
+                _disabledCreators.addAll(subs.filter { !urls.contains(it.url) });
+            }
+            filterCreators();
+        }
+
+        private fun filterCreators() {
+            val query = _searchBar.textSearch.text.toString().lowercase();
+            val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
+            val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) };
+
+            //Optimize
+            _enabledCreatorsFiltered.clear();
+            _enabledCreatorsFiltered.addAll(filteredEnabled);
+            _disabledCreatorsFiltered.clear();
+            _disabledCreatorsFiltered.addAll(filteredDisabled);
+
+            _recyclerCreatorsEnabled.notifyContentChanged();
+            _recyclerCreatorsDisabled.notifyContentChanged();
+        }
+
+        private fun enableCreator(channel: IPlatformChannel) {
+            val index = _disabledCreatorsFiltered.indexOf(channel);
+            if (index >= 0) {
+                _disabledCreators.remove(channel)
+                _disabledCreatorsFiltered.remove(channel);
+                _recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
+
+                _enabledCreators.add(channel);
+                _enabledCreatorsFiltered.add(channel);
+                _recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
+
+                _group?.let {
+                    if(!it.urls.contains(channel.url)) {
+                        it.urls.add(channel.url);
+                        save();
+                    }
+                }
+                updateMeta();
+            }
+        }
+        private fun disableCreator(channel: IPlatformChannel) {
+            val index = _enabledCreatorsFiltered.indexOf(channel);
+            if (index >= 0) {
+                _enabledCreators.remove(channel)
+                _enabledCreatorsFiltered.removeAt(index);
+                _recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
+
+                _disabledCreators.add(channel);
+                _disabledCreatorsFiltered.add(channel);
+                _recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
+
+                _group?.let {
+                    it.urls.remove(channel.url);
+                    save();
+                }
+                updateMeta();
+            }
+        }
+
+        private fun updateMeta() {
+            _textGroupMeta.text = "${_enabledCreators.size} creators";
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..52ed90ddbde22112f6b5ed258b25059106f0a3b7
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt
@@ -0,0 +1,141 @@
+package com.futo.platformplayer.fragment.mainactivity.main
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.CookieManager
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.FrameLayout
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UISlideOverlays
+import com.futo.platformplayer.activities.AddSourceOptionsActivity
+import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.states.StateSubscriptionGroups
+import com.futo.platformplayer.views.AnyAdapterView
+import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
+import com.futo.platformplayer.views.adapters.ItemMoveCallback
+import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupListViewHolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.Collections
+
+class SubscriptionGroupListFragment : MainFragment() {
+    override val isMainView : Boolean = true;
+    override val isTab: Boolean = true;
+    override val hasBottomBar: Boolean get() = true;
+
+    private var _touchHelper: ItemTouchHelper? = null;
+
+    private var _subs: ArrayList<SubscriptionGroup> = arrayListOf();
+    private var _list: AnyAdapterView<SubscriptionGroup, SubscriptionGroupListViewHolder>? = null;
+    private var _overlay: FrameLayout? = null;
+
+    override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        val view = inflater.inflate(R.layout.fragment_subscriptions_group_list, container, false);
+        _overlay = view.findViewById(R.id.overlay);
+        val recycler = view.findViewById<RecyclerView>(R.id.list);
+        val callback = ItemMoveCallback();
+        _touchHelper = ItemTouchHelper(callback);
+        _touchHelper?.attachToRecyclerView(recycler);
+
+        _subs.clear();
+        _subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
+        _list = recycler.asAny(_subs, RecyclerView.VERTICAL){
+            it.onClick.subscribe {
+                navigate<SubscriptionGroupFragment>(it);
+            };
+            it.onSettings.subscribe {
+
+            };
+            it.onDelete.subscribe { group ->
+                val loc = _subs.indexOf(group);
+                _subs.remove(group);
+                _list?.adapter?.notifyItemRangeRemoved(loc);
+                StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
+            };
+            it.onDragDrop.subscribe {
+                _touchHelper?.startDrag(it);
+            };
+        };
+
+        callback.onRowMoved.subscribe(::groupMoved);
+        return view;
+    }
+
+    private fun groupMoved(fromPosition: Int, toPosition: Int) {
+        Logger.i("SubscriptionGroupListFragment", "Moved ${fromPosition} to ${toPosition}");
+        synchronized(_subs) {
+            if (fromPosition < toPosition) {
+                for (i in fromPosition until toPosition) {
+                    Collections.swap(_subs, i, i + 1)
+                }
+            } else {
+                for (i in fromPosition downTo toPosition + 1) {
+                    Collections.swap(_subs, i, i - 1)
+                }
+            }
+        }
+        _list?.adapter?.notifyItemMoved(fromPosition, toPosition);
+
+        synchronized(_subs) {
+            for(i in 0 until _subs.size) {
+                val sub = _subs[i];
+                if(sub.priority != i) {
+                    sub.priority = i;
+                    StateSubscriptionGroups.instance.updateSubscriptionGroup(sub, true);
+                }
+            }
+        }
+    }
+
+
+    override fun onShownWithView(parameter: Any?, isBack: Boolean) {
+        super.onShownWithView(parameter, isBack);
+
+        updateGroups();
+
+        StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
+            updateGroups();
+        }
+
+        if(topBar is AddTopBarFragment)
+            (topBar as AddTopBarFragment).onAdd.subscribe {
+                _overlay?.let {
+                    UISlideOverlays.showCreateSubscriptionGroup(it)
+                }
+            };
+    }
+
+    private fun updateGroups() {
+        lifecycleScope.launch(Dispatchers.Main) {
+            _subs.clear();
+            _subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
+            _list?.adapter?.notifyContentChanged();
+        }
+    }
+
+    override fun onHide() {
+        super.onHide();
+        StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
+
+        if(topBar is AddTopBarFragment)
+            (topBar as AddTopBarFragment).onAdd.remove(this);
+    }
+
+    override fun onBackPressed(): Boolean {
+        return false;
+    }
+
+    companion object {
+        fun newInstance() = SubscriptionGroupListFragment().apply {}
+    }
+}
\ No newline at end of file
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 cd5dca38ab3555caf364664e108f684ba694ead8..41578d002cbaba958e2d953eb7b351cfea52b301 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
@@ -21,6 +21,7 @@ import com.futo.platformplayer.exceptions.ChannelException
 import com.futo.platformplayer.exceptions.RateLimitException
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.SearchType
+import com.futo.platformplayer.models.SubscriptionGroup
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.states.StateCache
 import com.futo.platformplayer.states.StatePlatform
@@ -99,6 +100,8 @@ class SubscriptionsFeedFragment : MainFragment() {
     class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
         override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
 
+        private var _subGroup: SubscriptionGroup? = null;
+
         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 ->
@@ -254,6 +257,18 @@ class SubscriptionsFeedFragment : MainFragment() {
                 layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
             };
             _subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
+            _subscriptionBar?.onToggleGroup?.subscribe { g ->
+                if(g is SubscriptionGroup.Add)
+                    UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
+                else {
+                    _subGroup = g;
+                    loadCache(); //TODO: Proper subset update
+                }
+            };
+            _subscriptionBar?.onHoldGroup?.subscribe { g ->
+                if(g !is SubscriptionGroup.Add)
+                    fragment.navigate<SubscriptionGroupFragment>(g);
+            };
 
             synchronized(_filterLock) {
                 _subscriptionBar?.setToggles(
@@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() {
 
         override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
             val nowSoon = OffsetDateTime.now().plusMinutes(5);
+            val filterGroup = _subGroup;
             return results.filter {
                 val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
 
+                //TODO: Check against a sub cache
+                if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
+                    return@filter false;
+
+
                 if(it.datetime?.isAfter(nowSoon) == true) {
                     if(!_filterSettings.allowPlanned)
                         return@filter false;
diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
index 1625b1e4969eeca556a2472fe305e970a1665a4d..9ca4aa8e17b229002e604380b79706d0a04ead48 100644
--- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
+++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
@@ -22,6 +22,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlR
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.others.Language
+import getHttpDataSourceFactory
 import kotlin.math.abs
 
 class VideoHelper {
diff --git a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt
index 1497b52daf4f6ff6af4009b9f9d0050f33161abf..1de1f917c3164b2e727c8dd2238f837b9bb321f9 100644
--- a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt
+++ b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt
@@ -1,14 +1,26 @@
 package com.futo.platformplayer.models
 
+import android.annotation.SuppressLint
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.widget.ImageView
 import com.bumptech.glide.Glide
+import com.futo.platformplayer.PresetImages
 import com.futo.platformplayer.R
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Transient
 import java.io.File
 
-data class ImageVariable(val url: String? = null, val resId: Int? = null, val bitmap: Bitmap? = null) {
+@kotlinx.serialization.Serializable
+data class ImageVariable(
+    val url: String? = null,
+    val resId: Int? = null,
+    @Transient
+    @Contextual
+    private val bitmap: Bitmap? = null,
+    val presetName: String? = null) {
 
+    @SuppressLint("DiscouragedApi")
     fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
         if(bitmap != null) {
             Glide.with(imageView)
@@ -23,6 +35,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
                 .load(url)
                 .placeholder(R.drawable.placeholder_channel_thumbnail)
                 .into(imageView);
+        } else if(!presetName.isNullOrEmpty()) {
+            val resId = PresetImages.getPresetResIdByName(presetName);
+            imageView.setImageResource(resId);
         } else if (fallbackResId != -1) {
             Glide.with(imageView)
                 .load(fallbackResId)
@@ -44,6 +59,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
         fun fromBitmap(bitmap: Bitmap): ImageVariable {
             return ImageVariable(null, null, bitmap);
         }
+        fun fromPresetName(str: String): ImageVariable {
+            return ImageVariable(null, null, null, str);
+        }
         fun fromFile(file: File): ImageVariable {
             return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
         }
diff --git a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c46aa3b8c2fbe548841cca5d7df65d40b800a6b5
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt
@@ -0,0 +1,33 @@
+package com.futo.platformplayer.models
+
+import java.util.UUID
+
+@kotlinx.serialization.Serializable
+open class SubscriptionGroup {
+    var id: String = UUID.randomUUID().toString();
+    var name: String;
+    var image: ImageVariable? = null;
+    var urls: MutableList<String> = mutableListOf();
+    var priority: Int = 99;
+
+    constructor(name: String) {
+        this.name = name;
+    }
+    constructor(parent: SubscriptionGroup) {
+        this.id = parent.id;
+        this.name = parent.name;
+        this.image = parent.image;
+        this.urls = parent.urls;
+        this.priority = parent.priority;
+    }
+
+    class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
+        var selected: Boolean = isSelected;
+    }
+
+    class Add: SubscriptionGroup("+") {
+        init {
+            urls.add("+");
+        }
+    }
+}
\ No newline at end of file
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 45e8e069a5ac869d251ffd58f65cc53d22728d16..bcf685928e1ffd9f911558fbdfe7e84aa33f8f25 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -133,6 +133,7 @@ class StateApp {
 
     //Files
     private var _tempDirectory: File? = null;
+    private var _persistentDirectory: File? = null;
 
 
     //AutoRotate
@@ -165,6 +166,16 @@ class StateApp {
         return File(_tempDirectory, name);
     }
 
+    fun getPersistFile(extension: String? = null): File {
+        val name = UUID.randomUUID().toString() +
+                if(extension != null)
+                    ".${extension}"
+                else
+                    "";
+
+        return File(_persistentDirectory, name);
+    }
+
     fun getCurrentSystemAutoRotate(): Boolean {
         _context?.let {
             systemAutoRotate = android.provider.Settings.System.getInt(
@@ -290,6 +301,10 @@ class StateApp {
                 _tempDirectory?.deleteRecursively();
             }
             _tempDirectory?.mkdirs();
+            _persistentDirectory = File(context.filesDir, "persist");
+            if(_persistentDirectory?.exists() == false) {
+                _persistentDirectory?.mkdirs();
+            }
         }
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt
index 8d9ceff642c41973fa57b83996d45b657d5f08a3..7b6628e68acea05818b1955140cbe30e2f7c27a5 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt
@@ -1,5 +1,6 @@
 package com.futo.platformplayer.states
 
+import android.content.Context
 import com.futo.platformplayer.UIDialogs
 import com.futo.platformplayer.api.http.server.ManagedHttpServer
 import com.futo.platformplayer.developer.DeveloperEndpoints
@@ -93,6 +94,13 @@ class StateDeveloper {
         }
     }
 
+    fun setDevClientSettings(settings: HashMap<String, String?>) {
+        val client = StatePlatform.instance.getDevClient();
+        client?.let {
+            it.descriptor.settings = settings;
+        };
+    }
+
 
     fun runServer() {
         if(_server != null)
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 85b16fd01e32b8cf75f840f244fa400f5ae68a51..2f08937d975ffca42c78a5ab7d4a064bc0b46102 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
 import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
 import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
+import com.futo.platformplayer.developer.DeveloperEndpoints
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.ImageVariable
 import com.futo.platformplayer.stores.FragmentedStorage
@@ -411,6 +412,16 @@ class StatePlugins {
 
     fun setPluginSettings(id: String, map: Map<String, String?>) {
         val newSettings = HashMap(map);
+        if(id == StateDeveloper.DEV_ID)
+        {
+            val decConfig = StatePlatform.instance.getDevClient()?.config ?: return;
+            for(setting in decConfig.settings) {
+                if(!newSettings.containsKey(setting.variableOrName) || newSettings[setting.variableOrName] == null)
+                    newSettings[setting.variableOrName] = setting.default;
+            }
+            StateDeveloper.instance.setDevClientSettings(newSettings);
+            return;
+        }
         val plugin = getPlugin(id);
 
         if(plugin != null) {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f77e2fad53229002811c0a55cce99695cc8e7770
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt
@@ -0,0 +1,94 @@
+package com.futo.platformplayer.states
+
+import com.futo.platformplayer.Settings
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.api.media.models.ResultCapabilities
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.api.media.models.channels.SerializedChannel
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
+import com.futo.platformplayer.api.media.platforms.js.JSClient
+import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.api.media.structures.*
+import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
+import com.futo.platformplayer.constructs.Event0
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.Event2
+import com.futo.platformplayer.engine.exceptions.PluginException
+import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
+import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
+import com.futo.platformplayer.exceptions.ChannelException
+import com.futo.platformplayer.findNonRuntimeException
+import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
+import com.futo.platformplayer.getNowDiffDays
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.polycentric.PolycentricCache
+import com.futo.platformplayer.resolveChannelUrl
+import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.stores.SubscriptionStorage
+import com.futo.platformplayer.stores.v2.ReconstructStore
+import com.futo.platformplayer.stores.v2.ManagedStore
+import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
+import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
+import kotlinx.coroutines.*
+import java.time.OffsetDateTime
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.ForkJoinPool
+import java.util.concurrent.ForkJoinTask
+import kotlin.collections.ArrayList
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlin.streams.asSequence
+import kotlin.streams.toList
+import kotlin.system.measureTimeMillis
+
+/***
+ * Used to maintain subscription groups
+ */
+class StateSubscriptionGroups {
+    private val _subGroups = FragmentedStorage.storeJson<SubscriptionGroup>("subscription_groups")
+        .withUnique { it.id }
+        .load();
+
+    val onGroupsChanged = Event0();
+
+    fun getSubscriptionGroup(id: String): SubscriptionGroup? {
+        return _subGroups.findItem { it.id == id };
+    }
+    fun getSubscriptionGroups(): List<SubscriptionGroup> {
+        return _subGroups.getItems();
+    }
+    fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) {
+        _subGroups.save(subGroup);
+        if(!preventNotify)
+            onGroupsChanged.emit();
+    }
+    fun deleteSubscriptionGroup(id: String){
+        val group = getSubscriptionGroup(id);
+        if(group != null) {
+            _subGroups.delete(group);
+            onGroupsChanged.emit();
+        }
+    }
+
+
+    companion object {
+        const val TAG = "StateSubscriptionGroups";
+        const val VERSION = 1;
+
+        private var _instance : StateSubscriptionGroups? = null;
+        val instance : StateSubscriptionGroups
+            get(){
+            if(_instance == null)
+                _instance = StateSubscriptionGroups();
+            return _instance!!;
+        };
+
+        fun finish() {
+            _instance?.let {
+                _instance = null;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt
index 2ff1e01fb53cc13b77c4a73ec4b6a56d95e7891c..db295aa38f2eb70f217671ca1ba9bc1a83e00aa2 100644
--- a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt
@@ -46,9 +46,10 @@ class AnyAdapterView<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I, T, T>,
         where T : AnyAdapter.AnyViewHolder<I>{
 
     companion object {
+        /*
         inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: List<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
             return asAny(ArrayList(list), orientation, reversed, onCreate);
-        }
+        }*/
         inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: ArrayList<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
             return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed);
         }
diff --git a/app/src/main/java/com/futo/platformplayer/views/SearchView.kt b/app/src/main/java/com/futo/platformplayer/views/SearchView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..77cf431e542ffb08529c14cc4b507c2291166e76
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/SearchView.kt
@@ -0,0 +1,33 @@
+package com.futo.platformplayer.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.widget.addTextChangedListener
+import com.futo.platformplayer.R
+import com.futo.platformplayer.constructs.Event0
+import com.futo.platformplayer.constructs.Event1
+
+class SearchView : FrameLayout {
+
+    val textSearch: TextView;
+    val buttonClear: ImageButton;
+
+    var onSearchChanged = Event1<String>();
+
+    constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
+        inflate(context, R.layout.view_search_bar, this);
+
+        textSearch = findViewById(R.id.edit_search)
+        buttonClear = findViewById(R.id.button_clear_search)
+
+        buttonClear.setOnClickListener { textSearch.text = "" };
+        textSearch.addTextChangedListener {
+            onSearchChanged.emit(it.toString());
+        };
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt
index 225c498b535291e4ca9f18db672dd82fbbd72457..d5ca59f0611a54858af3edab9451021d85f2b5a4 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt
@@ -1,7 +1,11 @@
 package com.futo.platformplayer.views.adapters
 
+import android.annotation.SuppressLint
 import android.view.View
 import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Filter
+import android.widget.Filterable
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import java.lang.reflect.Constructor
@@ -47,6 +51,7 @@ open class BaseAnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> {
             cb(item);
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     fun notifyContentChanged() {
         adapter.notifyDataSetChanged();
     }
@@ -116,7 +121,6 @@ class AnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>> : BaseAnyAdapter<I, T, T> {
     private class Adapter<I, T : AnyViewHolder<I>> : RecyclerView.Adapter<T> {
         private val _parent: AnyAdapter<I, T>;
 
-
         constructor(parentAdapter: AnyAdapter<I, T>) {
             _parent = parentAdapter;
         }
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b133875318e2ad41cc423abfe963b2b0888efaaf
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt
@@ -0,0 +1,164 @@
+package com.futo.platformplayer.views.adapters.viewholders
+
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.api.media.PlatformID
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.api.media.models.channels.SerializedChannel
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.polycentric.PolycentricCache
+import com.futo.platformplayer.selectBestImage
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.views.others.CreatorThumbnail
+import com.futo.polycentric.core.toURLInfoSystemLinkUrl
+
+class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<IPlatformChannel>(
+    LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
+
+    private val _creatorThumbnail: CreatorThumbnail;
+    private val _name: TextView;
+    private var _channel: IPlatformChannel? = null;
+
+    val onClick = Event1<IPlatformChannel>();
+
+    private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
+        StateApp.instance.scopeGetter,
+        { PolycentricCache.instance.getProfileAsync(it) })
+        .success { onProfileLoaded(it, true) }
+        .exception<Throwable> {
+            Logger.w(TAG, "Failed to load profile.", it);
+        };
+    
+    init {
+        _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
+        _name = _view.findViewById(R.id.text_channel_name);
+        _view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
+            val s = _channel ?: return@setOnClickListener;
+            onClick.emit(s);
+        }
+    }
+
+    override fun bind(value: IPlatformChannel) {
+        _taskLoadProfile.cancel();
+
+        _channel = value;
+
+        _creatorThumbnail.setThumbnail(value.thumbnail, false);
+        _name.text = value.name;
+
+        val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
+        if (cachedProfile != null) {
+            onProfileLoaded(cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(value.id);
+            }
+        } else {
+            _taskLoadProfile.run(value.id);
+        }
+    }
+
+    private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
+        val dp_55 = 55.dp(itemView.context.resources)
+        val profile = cachedPolycentricProfile?.profile;
+        val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
+            ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
+
+        if (avatar != null) {
+            _creatorThumbnail.setThumbnail(avatar, animate);
+        } else {
+            _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
+            _creatorThumbnail.setHarborAvailable(profile != null, animate);
+        }
+
+        if (profile != null) {
+            _name.text = profile.systemState.username;
+        }
+    }
+
+    companion object {
+        private const val TAG = "CreatorBarViewHolder";
+    }
+}
+class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SelectableCreatorBarViewHolder.Selectable>(
+    LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
+
+    private val _creatorThumbnail: CreatorThumbnail;
+    private val _name: TextView;
+    private var _channel: Selectable? = null;
+
+    val onClick = Event1<Selectable>();
+
+    private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
+        StateApp.instance.scopeGetter,
+        { PolycentricCache.instance.getProfileAsync(it) })
+        .success { onProfileLoaded(it, true) }
+        .exception<Throwable> {
+            Logger.w(TAG, "Failed to load profile.", it);
+        };
+
+    init {
+        _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
+        _name = _view.findViewById(R.id.text_channel_name);
+        _view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
+            val s = _channel ?: return@setOnClickListener;
+            onClick.emit(s);
+        }
+    }
+
+    override fun bind(value: Selectable) {
+        _taskLoadProfile.cancel();
+
+        _channel = value;
+
+        if(value.active)
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimaryDark, null))
+        else
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null))
+
+        _creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
+        _name.text = value.channel.name;
+
+        val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
+        if (cachedProfile != null) {
+            onProfileLoaded(cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(value.channel.id);
+            }
+        } else {
+            _taskLoadProfile.run(value.channel.id);
+        }
+    }
+
+    private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
+        val dp_55 = 55.dp(itemView.context.resources)
+        val profile = cachedPolycentricProfile?.profile;
+        val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
+            ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
+
+        if (avatar != null) {
+            _creatorThumbnail.setThumbnail(avatar, animate);
+        } else {
+            _creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
+            _creatorThumbnail.setHarborAvailable(profile != null, animate);
+        }
+
+        if (profile != null) {
+            _name.text = profile.systemState.username;
+        }
+    }
+
+    companion object {
+        private const val TAG = "CreatorBarViewHolder";
+    }
+
+    data class Selectable(var channel: IPlatformChannel, var active: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0f93a5eba28e53e99f87631b188d7e9fd4696a09
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt
@@ -0,0 +1,80 @@
+package com.futo.platformplayer.views.adapters.viewholders
+
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.api.media.PlatformID
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.polycentric.PolycentricCache
+import com.futo.platformplayer.selectBestImage
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.views.others.CreatorThumbnail
+import com.futo.polycentric.core.toURLInfoSystemLinkUrl
+import com.google.android.material.imageview.ShapeableImageView
+import com.google.android.material.shape.CornerFamily
+import com.google.android.material.shape.ShapeAppearanceModel
+
+class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
+    LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
+    private var _group: SubscriptionGroup? = null;
+
+    private val _image: ShapeableImageView;
+    private val _textSubGroup: TextView;
+
+    val onClick = Event1<SubscriptionGroup>();
+    val onClickLong = Event1<SubscriptionGroup>();
+
+    init {
+        _image = _view.findViewById(R.id.image);
+        _textSubGroup = _view.findViewById(R.id.text_sub_group);
+
+        val dp6 = 6.dp(_view.resources);
+        _image.shapeAppearanceModel = ShapeAppearanceModel.builder()
+            .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
+            .build()
+
+        _view.setOnClickListener {
+            _group?.let {
+                onClick.emit(it);
+            }
+        }
+        _view.setOnLongClickListener {
+            _group?.let {
+                onClickLong.emit(it);
+            }
+            true;
+        }
+    }
+
+    override fun bind(value: SubscriptionGroup) {
+        _group = value;
+        val img = value.image;
+        if(img != null)
+            img.setImageView(_image)
+        else {
+            _image.setImageResource(0);
+
+            if(value is SubscriptionGroup.Add)
+                _image.setBackgroundColor(Color.DKGRAY);
+        }
+        _textSubGroup.text = value.name;
+
+        if(value is SubscriptionGroup.Selectable && value.selected)
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
+        else
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
+    }
+
+    companion object {
+        private const val TAG = "SubscriptionGroupBarViewHolder";
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..19fe8a3071dd0bfdd3d4b39043ce81ea31cc4146
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt
@@ -0,0 +1,109 @@
+package com.futo.platformplayer.views.adapters.viewholders
+
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.api.media.PlatformID
+import com.futo.platformplayer.api.media.models.channels.SerializedChannel
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.polycentric.PolycentricCache
+import com.futo.platformplayer.selectBestImage
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.views.adapters.ItemMoveCallback
+import com.futo.platformplayer.views.others.CreatorThumbnail
+import com.futo.polycentric.core.toURLInfoSystemLinkUrl
+import com.google.android.material.imageview.ShapeableImageView
+import com.google.android.material.shape.CornerFamily
+import com.google.android.material.shape.ShapeAppearanceModel
+
+class SubscriptionGroupListViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
+    LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_subscription_group, _viewGroup, false)) {
+    private var _group: SubscriptionGroup? = null;
+
+    private val _thumb: ImageView;
+    private val _image: ShapeableImageView;
+    private val _textSubGroup: TextView;
+    private val _textSubGroupMeta: TextView;
+
+    private val _buttonSettings: ImageButton;
+    private val _buttonDelete: ImageButton;
+
+    val onClick = Event1<SubscriptionGroup>();
+    val onSettings = Event1<SubscriptionGroup>();
+    val onDelete = Event1<SubscriptionGroup>();
+    val onDragDrop = Event1<RecyclerView.ViewHolder>();
+
+    init {
+        _thumb = _view.findViewById(R.id.thumb);
+        _image = _view.findViewById(R.id.image);
+        _textSubGroup = _view.findViewById(R.id.text_sub_group);
+        _textSubGroupMeta = _view.findViewById(R.id.text_sub_group_meta);
+        _buttonSettings = _view.findViewById(R.id.button_settings);
+        _buttonDelete = _view.findViewById(R.id.button_trash);
+
+        val dp6 = 6.dp(_view.resources);
+        _image.shapeAppearanceModel = ShapeAppearanceModel.builder()
+            .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
+            .build()
+
+        _view.setOnClickListener {
+            _group?.let {
+                onClick.emit(it);
+            }
+        }
+        _thumb.setOnTouchListener { _, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) {
+                onDragDrop.emit(this);
+            }
+            false
+        };
+        _buttonSettings.setOnClickListener {
+            _group?.let {
+                onSettings.emit(it);
+            };
+        }
+        _buttonDelete.setOnClickListener {
+            _group?.let {
+                onDelete.emit(it);
+            };
+        }
+    }
+
+    override fun bind(value: SubscriptionGroup) {
+        _group = value;
+        val img = value.image;
+        if(img != null)
+            img.setImageView(_image)
+        else {
+            _image.setImageResource(0);
+
+            if(value is SubscriptionGroup.Add)
+                _image.setBackgroundColor(Color.DKGRAY);
+        }
+        _textSubGroup.text = value.name;
+        _textSubGroupMeta.text = "${value.urls.size} subscriptions";
+
+        if(value is SubscriptionGroup.Selectable && value.selected)
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
+        else
+            _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
+    }
+
+    companion object {
+        private const val TAG = "SubscriptionGroupBarViewHolder";
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0971d577e219f8e1a909d41953bf01caf12e2b57
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt
@@ -0,0 +1,234 @@
+package com.futo.platformplayer.views.overlays
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.shapes.Shape
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.futo.platformplayer.PresetImages
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.activities.IWithResultLauncher
+import com.futo.platformplayer.activities.MainActivity
+import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.constructs.Event0
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.models.ImageVariable
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateSubscriptions
+import com.futo.platformplayer.views.AnyAdapterView
+import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
+import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
+import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
+import com.futo.platformplayer.views.buttons.BigButton
+import com.github.dhaval2404.imagepicker.ImagePicker
+import com.google.android.flexbox.FlexboxLayout
+import com.google.android.material.imageview.ShapeableImageView
+import com.google.android.material.shape.CornerFamily
+import com.google.android.material.shape.ShapeAppearanceModel
+import java.io.File
+
+class ImageVariableOverlay: ConstraintLayout {
+    private val _buttonGallery: BigButton;
+    private val _imageGallerySelected: ImageView;
+    private val _imageGallerySelectedContainer: LinearLayout;
+    private val _buttonSelect: Button;
+    private val _topbar: OverlayTopbar;
+    private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
+    private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
+
+    private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
+    private val _presets: ArrayList<PresetImage> =
+        ArrayList(PresetImages.images.map { PresetImage(it.value, it.key, false) });
+
+    private var _selected: ImageVariable? = null;
+    private var _selectedFile: String? = null;
+
+    val onSelected = Event1<ImageVariable>();
+    val onClose = Event0();
+
+    constructor(context: Context, creatorFilters: List<String>? = null): super(context) {
+        val subs = StateSubscriptions.instance.getSubscriptions();
+        if(creatorFilters != null) {
+            _creators.addAll(subs
+                .filter { creatorFilters.contains(it.channel.url) }
+                .map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
+        }
+        else
+            _creators.addAll(subs
+                .map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
+        _recyclerCreators.notifyContentChanged();
+    }
+    constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
+    init {
+        inflate(context, R.layout.overlay_image_variable, this);
+        _topbar = findViewById(R.id.topbar);
+        _buttonGallery = findViewById(R.id.button_gallery);
+        _imageGallerySelected = findViewById(R.id.gallery_selected);
+        _imageGallerySelectedContainer = findViewById(R.id.gallery_selected_container);
+        _buttonSelect = findViewById(R.id.button_select);
+        _recyclerPresets = findViewById<RecyclerView>(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) {
+            it.onClick.subscribe {
+                _selected = ImageVariable.fromPresetName(it.name);
+                updateSelected();
+            };
+        };
+        val dp6 = 6.dp(resources);
+        _recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
+            creatorView.itemView.setPadding(0, dp6, 0, dp6);
+            creatorView.onClick.subscribe {
+                if(it.channel.thumbnail == null) {
+                    UIDialogs.toast(context, "No thumbnail found");
+                    return@subscribe;
+                }
+                _selected = ImageVariable(it.channel.thumbnail);
+                updateSelected();
+            };
+        };
+        _recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply {
+            this.orientation = LinearLayoutManager.VERTICAL;
+        };
+
+        _buttonGallery.onClick.subscribe {
+            val context = StateApp.instance.contextOrNull;
+            if(context is IWithResultLauncher && context is MainActivity) {
+                ImagePicker.with(context)
+                    .compress(512)
+                    .maxResultSize(750, 500)
+                    .createIntent {
+                        context.launchForResult(it, 888) {
+                            if(it.resultCode == Activity.RESULT_OK) {
+                                cleanupLastFile();
+                                val fileUri = it.data?.data;
+                                if(fileUri != null) {
+                                    val file = fileUri.toFile();
+                                    val ext = file.extension;
+                                    val persistFile = StateApp.instance.getPersistFile(ext);
+                                    file.copyTo(persistFile);
+                                    _selectedFile = persistFile.toUri().toString();
+                                    _selected = ImageVariable(_selectedFile);
+                                    updateSelected();
+                                }
+                            }
+                        };
+                    };
+            }
+        };
+        _imageGallerySelectedContainer.setOnClickListener {
+            if(_selectedFile != null) {
+                _selected = ImageVariable(_selectedFile);
+                updateSelected();
+            }
+        }
+        _buttonSelect.setOnClickListener {
+            _selected?.let {
+                select(it);
+            }
+        };
+        _topbar.onClose.subscribe {
+            onClose.emit();
+        }
+        updateSelected();
+    }
+
+    fun updateSelected() {
+        val id = _selected?.resId;
+        val name = _selected?.presetName;
+        val url = _selected?.url;
+        _presets.forEach { p -> p.active = p.name == name };
+        _recyclerPresets.notifyContentChanged();
+        _creators.forEach { p -> p.active = p.channel.thumbnail == url };
+        _recyclerCreators.notifyContentChanged();
+
+        if(_selectedFile != null) {
+            _imageGallerySelectedContainer.visibility = View.VISIBLE;
+            Glide.with(_imageGallerySelected)
+                .load(_selectedFile)
+                .into(_imageGallerySelected);
+        }
+        else
+            _imageGallerySelectedContainer.visibility = View.GONE;
+
+        if(_selected?.url == _selectedFile)
+            _imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.colorPrimary, null));
+        else
+            _imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.transparent, null));
+
+        if(_selected != null)
+            _buttonSelect.alpha = 1f;
+        else
+            _buttonSelect.alpha = 0.5f;
+    }
+    fun cleanupLastFile() {
+        _selectedFile?.let {
+            val file = File(it);
+            if(file.exists())
+                file.delete();
+            _selectedFile = null;
+        }
+    }
+
+
+    fun select(variable: ImageVariable) {
+        if(_selected?.url != _selectedFile)
+            cleanupLastFile();
+        onSelected.emit(variable);
+        onClose.emit();
+    }
+
+    class PresetViewHolder(viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<PresetImage>(LinearLayout(viewGroup.context)) {
+        private val view = _view as LinearLayout;
+        private val imageView = ShapeableImageView(viewGroup.context);
+
+        private var value: PresetImage = PresetImage(0, "", false);
+
+        val onClick = Event1<PresetImage>();
+        init {
+            view.addView(imageView);
+            val dp2 = 2.dp(viewGroup.context.resources);
+            val dp6 = 6.dp(viewGroup.context.resources);
+            view.setPadding(dp2, dp2, dp2, dp2);
+            imageView.setOnClickListener {
+                onClick.emit(value);
+            }
+            imageView.layoutParams = LinearLayout.LayoutParams(110.dp(viewGroup.context.resources), 70.dp(viewGroup.context.resources)).apply {
+                //this.rightMargin = dp6
+            }
+            imageView.scaleType = ImageView.ScaleType.CENTER_CROP
+            imageView.shapeAppearanceModel = ShapeAppearanceModel.builder()
+                .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
+                .build()
+        }
+
+        override fun bind(value: PresetImage) {
+            imageView.setImageResource(value.id);
+            this.value = value;
+            setActive(value.active);
+        }
+
+        fun setActive(active: Boolean) {
+            if(active)
+                _view.setBackgroundColor(view.context.resources.getColor(R.color.colorPrimary, null));
+            else
+                _view.setBackgroundColor(view.context.resources.getColor(R.color.transparent, null));
+        }
+    }
+
+    data class PresetImage(var id: Int, var name: String, var active: Boolean);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt
index ca4a39a98e89707b9064ff37816ebcf728b346a1..9a53fe44d8bfad672d33f931ce5433ffc5560bf1 100644
--- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt
@@ -18,7 +18,8 @@ class SlideUpMenuTextInput : LinearLayout {
     private lateinit var _editText: EditText;
     private lateinit var _inputMethodManager: InputMethodManager;
 
-    val text: String get() = _editText.text.toString();
+    var text: String get() = _editText.text.toString()
+        set(v: String) = _editText.setText(v);
 
     constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
         init();
diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt
index d001cdc56e49ef78d26cda28b4864d908c08e171..a456cba007270128deff160e86b602fb55160ea9 100644
--- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt
@@ -3,37 +3,113 @@ package com.futo.platformplayer.views.subscriptions
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.LinearLayout
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.RecyclerView
 import com.futo.platformplayer.R
+import com.futo.platformplayer.Settings
 import com.futo.platformplayer.api.media.models.channels.SerializedChannel
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.models.SubscriptionGroup
+import com.futo.platformplayer.states.StateSubscriptionGroups
 import com.futo.platformplayer.states.StateSubscriptions
 import com.futo.platformplayer.views.AnyAdapterView
 import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
 import com.futo.platformplayer.views.others.ToggleTagView
 import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
+import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 class SubscriptionBar : LinearLayout {
     private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
+    private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
     private val _tagsContainer: LinearLayout;
 
+    private val _groups: ArrayList<SubscriptionGroup>;
+    private var _group: SubscriptionGroup? = null;
+
     val onClickChannel = Event1<SerializedChannel>();
+    val onToggleGroup = Event1<SubscriptionGroup?>();
+    val onHoldGroup = Event1<SubscriptionGroup>();
 
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow();
+        StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
+            findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.Main) {
+                reloadGroups();
+            }
+        }
+    }
 
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
+    }
 
     constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
         inflate(context, R.layout.view_subscription_bar, this);
 
-        val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds };
+        val subscriptions = ArrayList(StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds });
         _adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
             it.onClick.subscribe { c ->
                 onClickChannel.emit(c.channel);
             };
         };
+        _groups = ArrayList(getGroups());
+        _subGroups = findViewById<RecyclerView>(R.id.recycler_subgroups).asAny(_groups, orientation = RecyclerView.HORIZONTAL) {
+            it.onClick.subscribe(::groupClicked);
+            it.onClickLong.subscribe { g ->
+                onHoldGroup.emit(g);
+            }
+        }
         _tagsContainer = findViewById(R.id.container_tags);
     }
 
+    private fun groupClicked(g: SubscriptionGroup) {
+        if(g is SubscriptionGroup.Add) {
+            onToggleGroup.emit(g);
+            return;
+        }
+        val isSame = _group == g;
+            _group?.let {
+                if (it is SubscriptionGroup.Selectable) {
+                    it.selected = false;
+                    val index = _groups.indexOf(it);
+                    if (index >= 0)
+                        _subGroups.notifyContentChanged(index);
+                }
+            }
+
+        if(isSame) {
+            _group = null;
+            onToggleGroup.emit(null);
+        }
+        else {
+            _group = g;
+            if(g is SubscriptionGroup.Selectable)
+                g.selected = true;
+            _subGroups.notifyContentChanged(_groups.indexOf(g));
+            onToggleGroup.emit(g);
+        }
+    }
+
+    private fun reloadGroups() {
+        val results = getGroups();
+        _groups.clear();
+        _groups.addAll(results);
+        _subGroups.notifyContentChanged();
+    }
+    private fun getGroups(): List<SubscriptionGroup> {
+        return if(Settings.instance.subscriptions.showSubscriptionGroups)
+            (StateSubscriptionGroups.instance.getSubscriptionGroups()
+                .sortedBy { it.priority }
+                .map {  SubscriptionGroup.Selectable(it, it.id == _group?.id) } +
+                    listOf(SubscriptionGroup.Add()));
+        else listOf();
+    }
+
 
     fun setToggles(vararg buttons: Toggle) {
         _tagsContainer.removeAllViews();
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
index abe86c622f2b0506b1701ac3566cfcd8140ebf68..25100c2f4b45d1be6344afe9494fd8c20a05399d 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
@@ -38,12 +38,14 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
+import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.helpers.VideoHelper
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.video.PlayerManager
+import getHttpDataSourceFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -402,22 +404,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
     @OptIn(UnstableApi::class)
     private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
         Logger.i(TAG, "Loading VideoSource [Url]");
-        _lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
-            .setUserAgent(DEFAULT_USER_AGENT))
+        val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
+            videoSource.getHttpDataSourceFactory()
+        else
+            DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
+        _lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
             .createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
     }
     @OptIn(UnstableApi::class)
     private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
         Logger.i(TAG, "Loading VideoSource [Dash]");
-        _lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
-            .setUserAgent(DEFAULT_USER_AGENT))
+        val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
+            videoSource.getHttpDataSourceFactory()
+        else
+            DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
+        _lastVideoMediaSource = DashMediaSource.Factory(dataSource)
             .createMediaSource(MediaItem.fromUri(videoSource.url))
     }
     @OptIn(UnstableApi::class)
     private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
         Logger.i(TAG, "Loading VideoSource [HLS]");
-        _lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
-            .setUserAgent(DEFAULT_USER_AGENT))
+        val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
+            videoSource.getHttpDataSourceFactory()
+        else
+            DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
+        _lastVideoMediaSource = HlsMediaSource.Factory(dataSource)
             .createMediaSource(MediaItem.fromUri(videoSource.url));
     }
 
@@ -455,15 +466,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
     @OptIn(UnstableApi::class)
     private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
         Logger.i(TAG, "Loading AudioSource [Url]");
-        _lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
-            .setUserAgent(DEFAULT_USER_AGENT))
+        val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
+            audioSource.getHttpDataSourceFactory()
+        else
+            DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
+        _lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
             .createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
     }
     @OptIn(UnstableApi::class)
     private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
         Logger.i(TAG, "Loading AudioSource [HLS]");
-        _lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
-            .setUserAgent(DEFAULT_USER_AGENT))
+        val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
+            audioSource.getHttpDataSourceFactory()
+        else
+            DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
+        _lastAudioMediaSource = HlsMediaSource.Factory(dataSource)
             .createMediaSource(MediaItem.fromUri(audioSource.url));
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
index d153c4405606ec9488baebb7f0bc39d73ef41f1a..4c99ccb9c47bef960eac2db5538e2f0492f253e8 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
+++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
@@ -11,6 +11,8 @@ import android.util.Log;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.futo.platformplayer.api.media.models.modifier.IRequest;
+import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
 import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
 import androidx.media3.common.C;
 import androidx.media3.common.PlaybackException;
@@ -60,7 +62,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
         private int readTimeoutMs;
         private boolean allowCrossProtocolRedirects;
         private boolean keepPostFor302Redirects;
-        @Nullable private JSRequestModifier requestModifier = null;
+        @Nullable private IRequestModifier requestModifier = null;
 
         /** Creates an instance. */
         public Factory() {
@@ -83,7 +85,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
          * @param requestModifier The request modifier that will be used, or {@code null} to use no request modifier
          * @return This factory.
          */
-        public Factory setRequestModifier(@Nullable JSRequestModifier requestModifier) {
+        public Factory setRequestModifier(@Nullable IRequestModifier requestModifier) {
             this.requestModifier = requestModifier;
             return this;
         }
@@ -228,7 +230,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
     private int responseCode;
     private long bytesToRead;
     private long bytesRead;
-    @Nullable private JSRequestModifier requestModifier;
+    @Nullable private IRequestModifier requestModifier;
 
     private JSHttpDataSource(
             @Nullable String userAgent,
@@ -238,7 +240,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
             @Nullable RequestProperties defaultRequestProperties,
             @Nullable Predicate<String> contentTypePredicate,
             boolean keepPostFor302Redirects,
-            @Nullable JSRequestModifier requestModifier) {
+            @Nullable IRequestModifier requestModifier) {
         super(/* isNetwork= */ true);
         this.userAgent = userAgent;
         this.connectTimeoutMillis = connectTimeoutMillis;
@@ -574,8 +576,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
 
         String requestUrl = url.toString();
         if (requestModifier != null) {
-            JSRequestModifier.IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
-            requestUrl = result.getUrl();
+            IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
+            String modifiedUrl = result.getUrl();
+            requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl;
             requestHeaders = result.getHeaders();
         }
 
diff --git a/app/src/main/res/drawable/background_primary_rounded_2dp.xml b/app/src/main/res/drawable/background_primary_rounded_2dp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1eafcfd944fe0d8f8676750d8324a8728771bd93
--- /dev/null
+++ b/app/src/main/res/drawable/background_primary_rounded_2dp.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/colorPrimary" />
+    <corners android:radius="1dp" />
+    <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml
new file mode 100644
index 0000000000000000000000000000000000000000..27d3ffc633997a267cd6db78d7b1a95f2fbbe39c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gallery.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M164.62,720Q137.96,720 118.98,701.02Q100,682.04 100,655.39L100,304.61Q100,277.96 118.98,258.98Q137.96,240 164.62,240L515.39,240Q542.04,240 561.02,258.98Q580,277.96 580,304.61L580,655.39Q580,682.04 561.02,701.02Q542.04,720 515.39,720L164.62,720ZM692.69,440Q678.39,440 669.19,430.81Q660,421.61 660,407.31L660,272.69Q660,258.38 669.19,249.19Q678.39,240 692.69,240L827.31,240Q841.62,240 850.81,249.19Q860,258.38 860,272.69L860,407.31Q860,421.61 850.81,430.81Q841.62,440 827.31,440L692.69,440ZM700,400L820,400L820,280L700,280L700,400ZM164.62,680L515.39,680Q526.15,680 533.08,673.08Q540,666.15 540,655.39L540,304.61Q540,293.85 533.08,286.92Q526.15,280 515.39,280L164.62,280Q153.85,280 146.92,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 146.92,673.08Q153.85,680 164.62,680ZM187.69,596.15L492.31,596.15L395,466.15L320,566.15L265,493.15L187.69,596.15ZM692.69,720Q678.39,720 669.19,710.81Q660,701.62 660,687.31L660,552.69Q660,538.39 669.19,529.19Q678.39,520 692.69,520L827.31,520Q841.62,520 850.81,529.19Q860,538.39 860,552.69L860,687.31Q860,701.62 850.81,710.81Q841.62,720 827.31,720L692.69,720ZM700,680L820,680L820,560L700,560L700,680ZM140,680Q140,680 140,673.08Q140,666.15 140,655.39L140,304.61Q140,293.85 140,286.92Q140,280 140,280L140,280Q140,280 140,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 140,673.08Q140,680 140,680L140,680ZM700,400L700,280L700,280L700,400L700,400ZM700,680L700,560L700,560L700,680L700,680Z"/>
+</vector>
diff --git a/app/src/main/res/drawable/xp_book.jpg b/app/src/main/res/drawable/xp_book.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..6ff22bb6d6e45182a78375714478c5f74906fc01
Binary files /dev/null and b/app/src/main/res/drawable/xp_book.jpg differ
diff --git a/app/src/main/res/drawable/xp_code.jpg b/app/src/main/res/drawable/xp_code.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..00dfb956d01715116212876f2e92b22a1f7c5b68
Binary files /dev/null and b/app/src/main/res/drawable/xp_code.jpg differ
diff --git a/app/src/main/res/drawable/xp_controller.jpg b/app/src/main/res/drawable/xp_controller.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..bd6aeaa06b210cd64c3a0c36eee6f0e197fd8958
Binary files /dev/null and b/app/src/main/res/drawable/xp_controller.jpg differ
diff --git a/app/src/main/res/drawable/xp_forest.jpg b/app/src/main/res/drawable/xp_forest.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e0428a2f44a45f198a9306c3d2c91a5d69f642e8
Binary files /dev/null and b/app/src/main/res/drawable/xp_forest.jpg differ
diff --git a/app/src/main/res/drawable/xp_laptop.jpg b/app/src/main/res/drawable/xp_laptop.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d33568c0d0b3a4dcfe6793b55f24deb94d283a35
Binary files /dev/null and b/app/src/main/res/drawable/xp_laptop.jpg differ
diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cf728852673abd8fdab6321d457129a1cb64d097
--- /dev/null
+++ b/app/src/main/res/layout/fragment_subscriptions_group.xml
@@ -0,0 +1,235 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:animateLayoutChanges="true">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="150dp"
+            android:background="#AAAAAA">
+            <ImageView
+                android:id="@+id/group_image_background"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:src="@drawable/xp_book"
+                android:scaleType="centerCrop" />
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="#AA000000">
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    app:layout_constraintTop_toTopOf="parent"
+                    app:layout_constraintRight_toRightOf="parent"
+                    android:orientation="horizontal">
+                    <ImageButton
+                        android:id="@+id/button_delete"
+                        android:layout_width="50dp"
+                        android:layout_height="50dp"
+                        android:layout_marginLeft="5dp"
+                        android:layout_marginRight="0dp"
+                        android:src="@drawable/ic_trash"
+                        app:tint="#CC0000"
+                        android:padding="10dp"
+                        android:background="@color/transparent"
+                        android:visibility="visible"
+                        android:scaleType="fitCenter" />
+
+                    <ImageButton
+                        android:id="@+id/button_settings"
+                        android:layout_width="50dp"
+                        android:layout_height="50dp"
+                        android:layout_marginLeft="5dp"
+                        android:layout_marginRight="5dp"
+                        android:src="@drawable/ic_settings"
+                        android:padding="10dp"
+                        android:background="@color/transparent"
+                        android:visibility="visible"
+                        android:scaleType="fitCenter" />
+                </LinearLayout>
+
+                <com.google.android.material.imageview.ShapeableImageView
+                    android:id="@+id/image_group"
+                    android:layout_width="110dp"
+                    android:layout_height="70dp"
+                    android:adjustViewBounds="true"
+                    app:circularflow_defaultRadius="10dp"
+                    android:layout_marginLeft="30dp"
+                    android:src="@drawable/xp_book"
+                    app:layout_constraintLeft_toLeftOf="parent"
+                    app:layout_constraintTop_toTopOf="parent"
+                    android:scaleType="centerCrop"
+                    app:layout_constraintBottom_toBottomOf="parent"  />
+
+                <LinearLayout
+                    android:id="@+id/button_edit_image"
+                    android:layout_width="30dp"
+                    android:layout_height="30dp"
+                    app:layout_constraintBottom_toTopOf="@id/image_group"
+                    app:layout_constraintLeft_toRightOf="@id/image_group"
+                    android:layout_marginLeft="-15dp"
+                    android:layout_marginBottom="-15dp"
+                    android:background="@drawable/background_pill">
+                    <ImageButton
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:padding="5dp"
+                        android:clickable="false"
+                        android:scaleType="fitCenter"
+                        android:background="@color/transparent"
+                        android:src="@drawable/ic_edit"/>
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    app:layout_constraintLeft_toRightOf="@id/image_group"
+                    app:layout_constraintTop_toTopOf="@id/image_group"
+                    app:layout_constraintBottom_toBottomOf="@id/image_group"
+                    app:layout_constraintRight_toRightOf="parent"
+                    android:layout_marginStart="25dp"
+                    android:layout_marginTop="10dp"
+                    android:orientation="vertical">
+                    <LinearLayout
+                        android:id="@+id/text_group_title_container"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content">
+                        <TextView
+                            android:id="@+id/text_group_title"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:fontFamily="@font/inter_bold"
+                            android:textSize="15dp"
+                            android:text="News" />
+                        <ImageButton
+                            android:layout_width="20dp"
+                            android:layout_height="20dp"
+                            android:padding="2dp"
+                            android:layout_marginStart="5dp"
+                            android:layout_marginBottom="-5dp"
+                            android:scaleType="fitCenter"
+                            android:background="@color/transparent"
+                            android:src="@drawable/ic_edit"/>
+                    </LinearLayout>
+                    <TextView
+                        android:id="@+id/text_group_meta"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:fontFamily="@font/inter_light"
+                        android:textSize="12dp"
+                        android:text="42 creators" />
+                </LinearLayout>
+            </androidx.constraintlayout.widget.ConstraintLayout>
+        </FrameLayout>
+
+        <com.futo.platformplayer.views.SearchView
+            android:id="@+id/search_bar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:paddingStart="0dp"
+                android:paddingEnd="0dp">
+                <LinearLayout
+                    android:id="@+id/container_enabled"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:layout_marginTop="10dp"
+                    android:layout_marginBottom="10dp">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:textSize="16dp"
+                        android:layout_marginLeft="20dp"
+                        android:layout_marginRight="20dp"
+                        android:textColor="@color/white"
+                        android:fontFamily="@font/inter_light"
+                        android:text="@string/enabled" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:textSize="12dp"
+                        android:layout_marginLeft="20dp"
+                        android:layout_marginRight="20dp"
+                        android:textColor="@color/gray_ac"
+                        android:fontFamily="@font/inter_extra_light"
+                        android:text="@string/these_creators_in_group" />
+
+                    <androidx.recyclerview.widget.RecyclerView
+                        android:id="@+id/recycler_creators_enabled"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="10dp"
+                        android:layout_marginLeft="5dp"
+                        android:layout_marginRight="5dp"
+                        android:paddingTop="10dp"
+                        android:paddingBottom="10dp" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:id="@+id/container_disabled"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:layout_marginTop="10dp">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:textSize="16dp"
+                        android:layout_marginLeft="20dp"
+                        android:layout_marginRight="20dp"
+                        android:textColor="@color/white"
+                        android:fontFamily="@font/inter_light"
+                        android:text="@string/disabled" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:textSize="12dp"
+                        android:layout_marginLeft="20dp"
+                        android:layout_marginRight="20dp"
+                        android:textColor="@color/gray_ac"
+                        android:fontFamily="@font/inter_extra_light"
+                        android:text="@string/these_creators_not_in_group" />
+
+                    <androidx.recyclerview.widget.RecyclerView
+                        android:id="@+id/recycler_creators_disabled"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="horizontal"
+                        android:layout_marginTop="10dp"
+                        android:paddingTop="10dp"
+                        android:paddingBottom="10dp" />
+                </LinearLayout>
+
+            </LinearLayout>
+        </ScrollView>
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_subscriptions_group_list.xml b/app/src/main/res/layout/fragment_subscriptions_group_list.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0c0b91b1fcf140cade61360c187ef478736e8e44
--- /dev/null
+++ b/app/src/main/res/layout/fragment_subscriptions_group_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+    </androidx.recyclerview.widget.RecyclerView>
+    <FrameLayout
+        android:id="@+id/overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone" />
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_subscription_group.xml b/app/src/main/res/layout/list_subscription_group.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d7c1d7bcfcc1909eb916bd2b7712fc4df4655b83
--- /dev/null
+++ b/app/src/main/res/layout/list_subscription_group.xml
@@ -0,0 +1,99 @@
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="70dp"
+    android:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:padding="2dp"
+    android:layout_margin="2dp"
+    android:clickable="true"
+    android:id="@+id/root">
+    <ImageView
+        android:id="@+id/thumb"
+        android:layout_width="50dp"
+        android:layout_height="match_parent"
+        android:padding="12dp"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:src="@drawable/ic_dragdrop_white"
+        android:background="@color/transparent"
+        />
+
+    <com.google.android.material.imageview.ShapeableImageView
+        android:id="@+id/image"
+        android:layout_width="75dp"
+        android:layout_height="50dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toRightOf="@id/thumb"
+        android:layout_marginLeft="10dp"
+        android:scaleType="centerCrop"
+        android:src="@drawable/xp_book" />
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        app:layout_constraintLeft_toRightOf="@id/image"
+        app:layout_constraintRight_toLeftOf="@id/buttons"
+        android:layout_marginLeft="10dp"
+        android:gravity="center"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/text_sub_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="5dp"
+            android:layout_marginRight="5dp"
+            android:maxLines="2"
+            android:ellipsize="end"
+            android:textSize="12dp"
+            android:textColor="@color/white"
+            android:textAlignment="textStart"
+            android:text="News" />
+        <TextView
+            android:id="@+id/text_sub_group_meta"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="5dp"
+            android:layout_marginRight="5dp"
+            android:maxLines="2"
+            android:ellipsize="end"
+            android:textSize="10dp"
+            android:textColor="@color/gray_ac"
+            android:textAlignment="textStart"
+            android:text="News" />
+    </LinearLayout>
+    <LinearLayout
+        android:id="@+id/buttons"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:orientation="horizontal">
+        <ImageButton
+            android:id="@+id/button_trash"
+            android:layout_width="50dp"
+            android:layout_height="match_parent"
+            android:layout_marginRight="10dp"
+            android:padding="10dp"
+            android:scaleType="fitCenter"
+            android:background="@color/transparent"
+            app:tint="@color/pastel_red"
+            android:src="@drawable/ic_trash"
+            />
+        <ImageButton
+            android:id="@+id/button_settings"
+            android:layout_width="50dp"
+            android:layout_height="match_parent"
+            android:padding="10dp"
+            android:scaleType="fitCenter"
+            android:background="@color/transparent"
+            android:src="@drawable/ic_settings"
+            android:visibility="gone"
+            />
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/overlay_image_variable.xml b/app/src/main/res/layout/overlay_image_variable.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b89d7fd458fdedb293fdff701541dabb160f57d8
--- /dev/null
+++ b/app/src/main/res/layout/overlay_image_variable.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:background="@color/black"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <com.futo.platformplayer.views.overlays.OverlayTopbar
+        android:id="@+id/topbar"
+        android:layout_width="match_parent"
+        android:layout_height="40dp"
+        app:title="Select an Image"
+        app:metadata=""
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent" />
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toBottomOf="@id/topbar"
+        app:layout_constraintBottom_toTopOf="@id/container_select"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:gravity="center_horizontal">
+
+            <LinearLayout
+                android:id="@+id/gallery_selected_container"
+                android:layout_width="150dp"
+                android:layout_height="100dp"
+                android:gravity="center_horizontal"
+                android:padding="2dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginLeft="10dp"
+                android:layout_marginRight="10dp">
+                <ImageView
+                    android:id="@+id/gallery_selected"
+                    android:layout_width="150dp"
+                    android:layout_height="100dp"
+                    android:scaleType="centerCrop"  />
+            </LinearLayout>
+            <com.futo.platformplayer.views.buttons.BigButton
+                android:id="@+id/button_gallery"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                app:buttonIcon="@drawable/ic_gallery"
+                app:buttonText="Open Photo Gallery"
+                app:buttonSubText="Pick an image from the gallery" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="10dp"
+                android:layout_marginRight="10dp"
+                android:layout_marginBottom="10dp"
+                android:orientation="vertical">
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:fontFamily="@font/inter_regular"
+                    android:textSize="20dp"
+                    android:text="Preset" />
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="10dp"
+                    android:textColor="@color/gray_ac"
+                    android:textSize="12dp"
+                    android:text="Pick a preset image" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/recycler_presets"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                </androidx.recyclerview.widget.RecyclerView>
+
+            </LinearLayout>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:layout_marginLeft="10dp"
+                android:layout_marginRight="10dp"
+                android:layout_marginBottom="10dp"
+                android:orientation="vertical">
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:fontFamily="@font/inter_regular"
+                    android:textSize="20dp"
+                    android:text="Creator" />
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="10dp"
+                    android:textColor="@color/gray_ac"
+                    android:textSize="12dp"
+                    android:text="Pick a creator as group image" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/recycler_creators"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                </androidx.recyclerview.widget.RecyclerView>
+            </LinearLayout>
+
+        </LinearLayout>
+    </ScrollView>
+
+    <LinearLayout
+        android:id="@+id/container_select"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent">
+        <Button
+            android:id="@+id/button_select"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/colorPrimary"
+            android:text="Select" />
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_search_bar.xml b/app/src/main/res/layout/view_search_bar.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c913c028de47821ae0317d6edc7192f1419ae67d
--- /dev/null
+++ b/app/src/main/res/layout/view_search_bar.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="40dp"
+    android:layout_margin="10dp"
+    android:orientation="vertical"
+    android:id="@+id/root">
+
+
+    <EditText
+        android:id="@+id/edit_search"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inputType="text"
+        android:imeOptions="actionDone"
+        android:singleLine="true"
+        android:hint="Search"
+        android:paddingEnd="46dp" />
+
+    <ImageButton
+        android:id="@+id/button_clear_search"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingStart="18dp"
+        android:paddingEnd="18dp"
+        android:layout_gravity="right|center_vertical"
+        android:visibility="invisible"
+        android:src="@drawable/ic_clear_16dp" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_subscription_bar.xml b/app/src/main/res/layout/view_subscription_bar.xml
index f08bb7dfbd8319b0336790288325513cd640a646..5a11c82aa176bc69c2656713087fb00d89a74f30 100644
--- a/app/src/main/res/layout/view_subscription_bar.xml
+++ b/app/src/main/res/layout/view_subscription_bar.xml
@@ -23,4 +23,9 @@
             android:layout_height="wrap_content"
             android:orientation="horizontal" />
     </ScrollView>
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler_subgroups"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" />
 </LinearLayout>
diff --git a/app/src/main/res/layout/view_subscription_group_bar.xml b/app/src/main/res/layout/view_subscription_group_bar.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8dc1f27aeeeff7a41215ff07792e02721e450bd5
--- /dev/null
+++ b/app/src/main/res/layout/view_subscription_group_bar.xml
@@ -0,0 +1,36 @@
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="78dp"
+    android:layout_height="54dp"
+    android:orientation="vertical"
+    android:gravity="center_horizontal"
+    android:padding="2dp"
+    android:layout_margin="2dp"
+    android:clickable="true"
+    android:id="@+id/root">
+    <com.google.android.material.imageview.ShapeableImageView
+        android:id="@+id/image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:src="@drawable/xp_book" />
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#99000000"
+        android:gravity="center">
+        <TextView
+            android:id="@+id/text_sub_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="5dp"
+            android:layout_marginRight="5dp"
+            android:maxLines="2"
+            android:ellipsize="end"
+            android:textSize="12dp"
+            android:textAlignment="center"
+            android:text="News" />
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/view_toggle_tag.xml b/app/src/main/res/layout/view_toggle_tag.xml
index 0992886c750e328b3b98dc68b87349568dc85340..886f2de5c871e9641686ebeb174c2924a9557c33 100644
--- a/app/src/main/res/layout/view_toggle_tag.xml
+++ b/app/src/main/res/layout/view_toggle_tag.xml
@@ -2,7 +2,7 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="wrap_content"
-    android:layout_height="27dp"
+    android:layout_height="32dp"
     android:paddingStart="15dp"
     android:paddingEnd="15dp"
     android:background="@drawable/background_pill"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index da1c3f6edb88118bd435266f6819904d8c596a03..9b50c55294aea350cb0cda7b364795c7f99de278 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -69,6 +69,8 @@
     <string name="discover">Discover</string>
     <string name="find_new_video_sources_to_add">Find new video sources to add</string>
     <string name="these_sources_have_been_disabled">These sources have been disabled</string>
+    <string name="these_creators_in_group">These are the creators that are visible for this group.</string>
+    <string name="these_creators_not_in_group">These creators are not in this group.</string>
     <string name="disabled">Disabled</string>
     <string name="watch_later">Watch Later</string>
     <string name="create">Create</string>
@@ -346,6 +348,9 @@
     <string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
     <string name="background_switch_audio">Switch to Audio in Background</string>
     <string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
+    <string name="subscription_group_menu">Groups</string>
+    <string name="show_subscription_group">Show Subscription Groups</string>
+    <string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
     <string name="preview_feed_items">Preview Feed Items</string>
     <string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
     <string name="log_level">Log Level</string>
@@ -564,6 +569,7 @@
     <string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string>
     <string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string>
     <string name="create_new_playlist">Create new playlist</string>
+    <string name="create_new_subgroup">Create new subscription group</string>
     <string name="expected_media_content_found">Expected media content, found</string>
     <string name="failed_to_load_post">Failed to load post.</string>
     <string name="replies">replies</string>