diff --git a/src/renderer/components/SubscriptionSettings/SubscriptionSettings.vue b/src/renderer/components/SubscriptionSettings/SubscriptionSettings.vue
index 2e7e6f6935816..0bb0b21132619 100644
--- a/src/renderer/components/SubscriptionSettings/SubscriptionSettings.vue
+++ b/src/renderer/components/SubscriptionSettings/SubscriptionSettings.vue
@@ -24,6 +24,13 @@
compact
@change="updateUnsubscriptionPopupStatus"
/>
+
} */
+const limitRequestFallbackWithoutRss = computed(() => store.getters.getLimitRequestFallbackWithoutRss)
+
+/**
+ * @param {boolean} value
+ */
+function updateLimitRequestFallbackWithoutRss(value) {
+ store.dispatch('updateLimitRequestFallbackWithoutRss', value)
+}
+
/** @type {import('vue').ComputedRef} */
const onlyShowLatestFromChannel = computed(() => store.getters.getOnlyShowLatestFromChannel)
diff --git a/src/renderer/components/SubscriptionsLive.vue b/src/renderer/components/SubscriptionsLive.vue
index 4bea31baf8386..7be9e9e9732bc 100644
--- a/src/renderer/components/SubscriptionsLive.vue
+++ b/src/renderer/components/SubscriptionsLive.vue
@@ -54,6 +54,9 @@ const subscriptionCacheReady = computed(() => store.getters.getSubscriptionCache
/** @type {import('vue').ComputedRef} */
const useRssFeeds = computed(() => store.getters.getUseRssFeeds)
+/** @type {import('vue').ComputedRef} */
+const limitRequestFallbackWithoutRss = computed(() => store.getters.getLimitRequestFallbackWithoutRss)
+
/** @type {import('vue').ComputedRef} */
const fetchSubscriptionsAutomatically = computed(() => store.getters.getFetchSubscriptionsAutomatically)
@@ -185,14 +188,22 @@ async function loadVideosForSubscriptionsFromRemote() {
let channelCount = 0
isLoading.value = true
- let useRss = useRssFeeds.value
- if (channelsToLoadFromRemote.length >= 125 && !useRss) {
- showToast(
- t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
- 10000
- )
- useRss = true
- }
+ const useRss = limitRequestFallbackWithoutRss.value
+ ? useRssFeeds.value
+ : (() => {
+ let rss = useRssFeeds.value
+ if (channelsToLoadFromRemote.length >= 125 && !rss) {
+ showToast(
+ t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
+ 10000
+ )
+ rss = true
+ }
+ return rss
+ })()
+
+ const CHUNK_SIZE = 80
+ const CHUNK_DELAY_MS = 2000
store.commit('setShowProgressBar', true)
store.commit('setProgressBarPercentage', 0)
@@ -200,8 +211,9 @@ async function loadVideosForSubscriptionsFromRemote() {
errorChannels.value = []
const subscriptionUpdates = []
+ const videoListFromRemote = []
- const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
+ const processChannel = async (channel) => {
let videos, name, thumbnailUrl
if (!process.env.SUPPORTS_LOCAL_API || backendPreference.value === 'invidious') {
@@ -238,7 +250,22 @@ async function loadVideosForSubscriptionsFromRemote() {
}
return videos ?? []
- }))).flat()
+ }
+
+ if (limitRequestFallbackWithoutRss.value && !useRss) {
+ for (let i = 0; i < channelsToLoadFromRemote.length; i += CHUNK_SIZE) {
+ if (i > 0) {
+ await new Promise(resolve => setTimeout(resolve, CHUNK_DELAY_MS))
+ }
+
+ const chunk = channelsToLoadFromRemote.slice(i, i + CHUNK_SIZE)
+ const chunkResults = await Promise.all(chunk.map(processChannel))
+ videoListFromRemote.push(...chunkResults.flat())
+ }
+ } else {
+ const results = await Promise.all(channelsToLoadFromRemote.map(processChannel))
+ videoListFromRemote.push(...results.flat())
+ }
videoList.value = updateVideoListAfterProcessing(videoListFromRemote)
isLoading.value = false
diff --git a/src/renderer/components/SubscriptionsVideos.vue b/src/renderer/components/SubscriptionsVideos.vue
index 049a1741a07fd..7d8afc9fbdefe 100644
--- a/src/renderer/components/SubscriptionsVideos.vue
+++ b/src/renderer/components/SubscriptionsVideos.vue
@@ -54,6 +54,9 @@ const subscriptionCacheReady = computed(() => store.getters.getSubscriptionCache
/** @type {import('vue').ComputedRef} */
const useRssFeeds = computed(() => store.getters.getUseRssFeeds)
+/** @type {import('vue').ComputedRef} */
+const limitRequestFallbackWithoutRss = computed(() => store.getters.getLimitRequestFallbackWithoutRss)
+
/** @type {import('vue').ComputedRef} */
const fetchSubscriptionsAutomatically = computed(() => store.getters.getFetchSubscriptionsAutomatically)
@@ -184,14 +187,22 @@ async function loadVideosForSubscriptionsFromRemote() {
let channelCount = 0
isLoading.value = true
- let useRss = useRssFeeds.value
- if (channelsToLoadFromRemote.length >= 125 && !useRss) {
- showToast(
- t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
- 10000
- )
- useRss = true
- }
+ const useRss = limitRequestFallbackWithoutRss.value
+ ? useRssFeeds.value
+ : (() => {
+ let rss = useRssFeeds.value
+ if (channelsToLoadFromRemote.length >= 125 && !rss) {
+ showToast(
+ t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
+ 10000
+ )
+ rss = true
+ }
+ return rss
+ })()
+
+ const CHUNK_SIZE = 80
+ const CHUNK_DELAY_MS = 2000
store.commit('setShowProgressBar', true)
store.commit('setProgressBarPercentage', 0)
@@ -199,8 +210,9 @@ async function loadVideosForSubscriptionsFromRemote() {
errorChannels.value = []
const subscriptionUpdates = []
+ const videoListFromRemote = []
- const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
+ const processChannel = async (channel) => {
let videos, name, thumbnailUrl
if (!process.env.SUPPORTS_LOCAL_API || backendPreference.value === 'invidious') {
@@ -237,7 +249,22 @@ async function loadVideosForSubscriptionsFromRemote() {
}
return videos ?? []
- }))).flat()
+ }
+
+ if (limitRequestFallbackWithoutRss.value && !useRss) {
+ for (let i = 0; i < channelsToLoadFromRemote.length; i += CHUNK_SIZE) {
+ if (i > 0) {
+ await new Promise(resolve => setTimeout(resolve, CHUNK_DELAY_MS))
+ }
+
+ const chunk = channelsToLoadFromRemote.slice(i, i + CHUNK_SIZE)
+ const chunkResults = await Promise.all(chunk.map(processChannel))
+ videoListFromRemote.push(...chunkResults.flat())
+ }
+ } else {
+ const results = await Promise.all(channelsToLoadFromRemote.map(processChannel))
+ videoListFromRemote.push(...results.flat())
+ }
videoList.value = updateVideoListAfterProcessing(videoListFromRemote)
isLoading.value = false
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index c0915813b923b..27e7b8b7b9e62 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -280,6 +280,7 @@ const state = {
useProxy: false,
userPlaylistSortOrder: 'date_added_descending',
useRssFeeds: false,
+ limitRequestFallbackWithoutRss: false,
useSponsorBlock: false,
videoVolumeMouseScroll: false,
videoPlaybackRateMouseScroll: false,
diff --git a/src/renderer/views/Subscriptions/Subscriptions.vue b/src/renderer/views/Subscriptions/Subscriptions.vue
index 6f36d1111e011..18a71be771847 100644
--- a/src/renderer/views/Subscriptions/Subscriptions.vue
+++ b/src/renderer/views/Subscriptions/Subscriptions.vue
@@ -167,6 +167,11 @@ const useRssFeeds = computed(() => {
return store.getters.getUseRssFeeds
})
+/** @type {import('vue').ComputedRef} */
+const limitRequestFallbackWithoutRss = computed(() => {
+ return store.getters.getLimitRequestFallbackWithoutRss
+})
+
/** @type {import('vue').Ref<'videos' | 'shorts' | 'live' | 'community' | null>} */
const currentTab = ref('videos')
@@ -196,7 +201,7 @@ const visibleTabs = computed(() => {
}
// community does not support rss
- if (!hideSubscriptionsCommunity.value && !useRssFeeds.value && activeSubscriptionList.value.length < 125) {
+ if (!hideSubscriptionsCommunity.value && !useRssFeeds.value && (limitRequestFallbackWithoutRss.value || activeSubscriptionList.value.length < 125)) {
tabs.push('community')
}
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index 8bbf28982fa0f..cbc883dce06e5 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -537,6 +537,7 @@ Settings:
'Limit the number of videos displayed for each channel': 'Limit the number of videos displayed for each channel'
To: To
Confirm Before Unsubscribing: Confirm Before Unsubscribing
+ Limit Request Fallback Without RSS: Limit Request Fallback Without RSS
Distraction Free Settings:
Distraction Free Settings: Distraction Free
Sections:
@@ -1086,6 +1087,10 @@ Tooltips:
but doesn't provide certain information like video duration, live status or posts
Fetch Automatically: When enabled, FreeTube will automatically fetch
your subscription feed on startup and when a new window is opened.
+ Limit Request Fallback Without RSS: When enabled, FreeTube will split
+ subscription requests into smaller chunks instead of forcing RSS for large
+ subscription lists. This prevents rate limiting while preserving video duration,
+ live status and other metadata that RSS does not provide
Experimental Settings:
Replace HTTP Cache: Disables Electron's disk based HTTP cache and enables a custom in-memory image cache. Will lead to increased RAM usage.
SponsorBlock Settings: