From 1d08562e4334b8f9e4e07f68d855abd7d5032fb5 Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Sun, 15 Feb 2026 13:20:08 -0600 Subject: [PATCH 1/6] Add SponsorBlock unskip and reskip feature --- .../ft-shaka-video-player.css | 6 ++ .../ft-shaka-video-player.js | 84 +++++++++++++++---- .../ft-shaka-video-player.vue | 6 +- static/locales/en-US.yaml | 2 + 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css index e1f3d79055cfd..5575f2132c0ec 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css @@ -145,6 +145,12 @@ border-radius: 20px; background-color: rgb(0 0 0 / 80%); color: #fff; + text-align: center; +} + +.unskipInstruction { + font-size: 0.8em; + opacity: 0.8; } :deep(.markerContainer) { diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 02ee2146bf928..e9c05ab44be71 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -479,9 +479,10 @@ export default defineComponent({ /** * Yes a map would be much more suitable for this (unlike objects they retain the order that items were inserted), * but Vue 2 doesn't support reactivity on Maps, so we have to use an array instead - * @type {import('vue').Ref<{uuid: string, translatedCategory: string, timeoutId: number}[]>} + * @type {import('vue').Ref<{uuid: string, translatedCategory: string, timeoutId: number, startTime: number, endTime: number, unskipped: boolean}[]>} */ const skippedSponsorBlockSegments = ref([]) + const ignoredSponsorBlockSegments = ref(new Set()) async function setupSponsorBlock() { let segments, averageDuration @@ -511,6 +512,14 @@ export default defineComponent({ * @param {number} currentTime */ function skipSponsorBlockSegments(currentTime) { + skippedSponsorBlockSegments.value = skippedSponsorBlockSegments.value.filter(segment => { + if (segment.unskipped && (currentTime >= segment.endTime || currentTime < segment.startTime)) { + ignoredSponsorBlockSegments.value.delete(segment.uuid) + return false + } + return true + }) + const { autoSkip } = sponsorSkips.value if (autoSkip.size === 0) { @@ -523,7 +532,7 @@ export default defineComponent({ const skippedSegments = [] sponsorBlockSegments.forEach(segment => { - if (autoSkip.has(segment.category) && currentTime < segment.endTime && + if (!ignoredSponsorBlockSegments.value.has(segment.uuid) && autoSkip.has(segment.category) && currentTime < segment.endTime && (segment.startTime <= currentTime || // if we already have a segment to skip, check if there are any that are less than 150ms later, // so that we can skip them all in one go (especially useful on slow connections) @@ -550,26 +559,46 @@ export default defineComponent({ video_.currentTime = newTime if (sponsorBlockShowSkippedToast.value) { - skippedSegments.forEach(({ uuid, category }) => { - // if the element already exists, just update the timeout, instead of creating a duplicate - // can happen at the end of the video sometimes + skippedSegments.forEach(({ uuid, category, startTime, endTime }) => { + const removeSegment = () => { + const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) + if (index !== -1) { + const segment = skippedSponsorBlockSegments.value[index] + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + skippedSponsorBlockSegments.value.splice(index, 1) + } + ignoredSponsorBlockSegments.value.delete(uuid) + } + const existingSkip = skippedSponsorBlockSegments.value.find(skipped => skipped.uuid === uuid) if (existingSkip) { clearTimeout(existingSkip.timeoutId) + clearInterval(existingSkip.intervalId) - existingSkip.timeoutId = setTimeout(() => { - const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) - skippedSponsorBlockSegments.value.splice(index, 1) - }, 2000) + existingSkip.timeLeft = 4 + existingSkip.intervalId = setInterval(() => { + existingSkip.timeLeft-- + }, 1000) + existingSkip.timeoutId = setTimeout(removeSegment, 4000) } else { - skippedSponsorBlockSegments.value.push({ + const segmentObj = reactive({ uuid, translatedCategory: translateSponsorBlockCategory(category), - timeoutId: setTimeout(() => { - const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) - skippedSponsorBlockSegments.value.splice(index, 1) - }, 2000) + startTime, + endTime, + unskipped: false, + timeLeft: 4, + intervalId: null, + timeoutId: null }) + + segmentObj.intervalId = setInterval(() => { + segmentObj.timeLeft-- + }, 1000) + segmentObj.timeoutId = setTimeout(removeSegment, 4000) + + skippedSponsorBlockSegments.value.push(segmentObj) } }) } @@ -2292,6 +2321,28 @@ export default defineComponent({ } switch (event.key.toLowerCase()) { + case 'enter': { + if (skippedSponsorBlockSegments.value.length > 0) { + event.preventDefault() + const segment = skippedSponsorBlockSegments.value[skippedSponsorBlockSegments.value.length - 1] + if (segment.unskipped) { + video_.currentTime = segment.endTime + const index = skippedSponsorBlockSegments.value.indexOf(segment) + if (index !== -1) { + skippedSponsorBlockSegments.value.splice(index, 1) + } + ignoredSponsorBlockSegments.value.delete(segment.uuid) + } else { + video_.currentTime = segment.startTime + segment.unskipped = true + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + segment.timeLeft = null + ignoredSponsorBlockSegments.value.add(segment.uuid) + } + } + break + } case ' ': case 'spacebar': // older browsers might return spacebar instead of a space character case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.PLAY: @@ -3171,7 +3222,10 @@ export default defineComponent({ navigator.mediaSession.playbackState = 'none' } - skippedSponsorBlockSegments.value.forEach(segment => clearTimeout(segment.timeoutId)) + skippedSponsorBlockSegments.value.forEach(segment => { + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + }) window.removeEventListener('online', onlineHandler) window.removeEventListener('offline', offlineHandler) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue index 8f49114874dfb..bc5f7b321a737 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue @@ -116,11 +116,15 @@ class="skippedSegmentsWrapper" >

{{ $t('Video.Player.Skipped segment', { segmentCategory: translatedCategory }) }} +
+ + {{ unskipped ? $t('Video.Player.Press Enter to reskip') : $t('Video.Player.Press Enter to unskip', { timeLeft }) }} +

diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 95aedd31f9d06..5362bac3477f1 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -937,6 +937,8 @@ Video: You appear to be offline: You appear to be offline. Playback will resume automatically when your connection comes back: Playback will resume automatically when your connection comes back. Skipped segment: 'Skipped {segmentCategory} segment' + Press Enter to unskip: 'Press Enter to unskip ({timeLeft}s)' + Press Enter to reskip: 'Press Enter to reskip' Watch: 'Remaining preroll-ad time: {remindingTimeSeconds}s': 'Remaining preroll-ad time: {remindingTimeSeconds}s' 'Remaining SABR backoff time: {remindingTimeSeconds}s': 'Remaining SABR backoff time: {remindingTimeSeconds}s' From 48f9971c5d234482fe7e476e7f8da655e2ec705d Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Tue, 24 Feb 2026 20:04:37 -0600 Subject: [PATCH 2/6] Use reactive instead of ref for ignoredSponsorBlockSegments --- .../ft-shaka-video-player/ft-shaka-video-player.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index e9c05ab44be71..3791f914d9aca 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -482,7 +482,7 @@ export default defineComponent({ * @type {import('vue').Ref<{uuid: string, translatedCategory: string, timeoutId: number, startTime: number, endTime: number, unskipped: boolean}[]>} */ const skippedSponsorBlockSegments = ref([]) - const ignoredSponsorBlockSegments = ref(new Set()) + const ignoredSponsorBlockSegments = reactive(new Set()) async function setupSponsorBlock() { let segments, averageDuration @@ -514,7 +514,7 @@ export default defineComponent({ function skipSponsorBlockSegments(currentTime) { skippedSponsorBlockSegments.value = skippedSponsorBlockSegments.value.filter(segment => { if (segment.unskipped && (currentTime >= segment.endTime || currentTime < segment.startTime)) { - ignoredSponsorBlockSegments.value.delete(segment.uuid) + ignoredSponsorBlockSegments.delete(segment.uuid) return false } return true @@ -532,7 +532,7 @@ export default defineComponent({ const skippedSegments = [] sponsorBlockSegments.forEach(segment => { - if (!ignoredSponsorBlockSegments.value.has(segment.uuid) && autoSkip.has(segment.category) && currentTime < segment.endTime && + if (!ignoredSponsorBlockSegments.has(segment.uuid) && autoSkip.has(segment.category) && currentTime < segment.endTime && (segment.startTime <= currentTime || // if we already have a segment to skip, check if there are any that are less than 150ms later, // so that we can skip them all in one go (especially useful on slow connections) @@ -568,7 +568,7 @@ export default defineComponent({ clearInterval(segment.intervalId) skippedSponsorBlockSegments.value.splice(index, 1) } - ignoredSponsorBlockSegments.value.delete(uuid) + ignoredSponsorBlockSegments.delete(uuid) } const existingSkip = skippedSponsorBlockSegments.value.find(skipped => skipped.uuid === uuid) @@ -2331,14 +2331,14 @@ export default defineComponent({ if (index !== -1) { skippedSponsorBlockSegments.value.splice(index, 1) } - ignoredSponsorBlockSegments.value.delete(segment.uuid) + ignoredSponsorBlockSegments.delete(segment.uuid) } else { video_.currentTime = segment.startTime segment.unskipped = true clearTimeout(segment.timeoutId) clearInterval(segment.intervalId) segment.timeLeft = null - ignoredSponsorBlockSegments.value.add(segment.uuid) + ignoredSponsorBlockSegments.add(segment.uuid) } } break From a229e6e36024d52d7b906e16aeccbb4963f5e8c2 Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Tue, 24 Feb 2026 20:40:25 -0600 Subject: [PATCH 3/6] Fix dynamically creating a reactive object --- .../ft-shaka-video-player.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 3791f914d9aca..2bab1f669915e 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -582,7 +582,7 @@ export default defineComponent({ }, 1000) existingSkip.timeoutId = setTimeout(removeSegment, 4000) } else { - const segmentObj = reactive({ + const segmentObj = { uuid, translatedCategory: translateSponsorBlockCategory(category), startTime, @@ -591,14 +591,15 @@ export default defineComponent({ timeLeft: 4, intervalId: null, timeoutId: null - }) - - segmentObj.intervalId = setInterval(() => { - segmentObj.timeLeft-- - }, 1000) - segmentObj.timeoutId = setTimeout(removeSegment, 4000) + } skippedSponsorBlockSegments.value.push(segmentObj) + const reactiveSegmentObj = skippedSponsorBlockSegments.value.find(skipped => skipped.uuid === uuid) + + reactiveSegmentObj.intervalId = setInterval(() => { + reactiveSegmentObj.timeLeft-- + }, 1000) + reactiveSegmentObj.timeoutId = setTimeout(removeSegment, 4000) } }) } From 9fa2f538114277d5c81714d6ee7218cac4f23a05 Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Tue, 3 Mar 2026 15:40:59 -0600 Subject: [PATCH 4/6] Register keyboard shortcut correctly --- src/constants.js | 1 + .../FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue | 1 + .../components/ft-shaka-video-player/ft-shaka-video-player.js | 2 +- static/locales/en-US.yaml | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 7c61599a0ea7b..c641a9489d24f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -174,6 +174,7 @@ const KeyboardShortcuts = { VOLUME_DOWN: 'arrowdown', STATS: 'd', TAKE_SCREENSHOT: 'u', + UNSKIP_SPONSOR_BLOCK: 'enter', }, PLAYBACK: { PLAY: 'k', diff --git a/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue b/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue index 2f666993ce242..becf929541cf0 100644 --- a/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue +++ b/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue @@ -144,6 +144,7 @@ const localizedShortcutNameToShortcutsMappings = computed(() => { [t('KeyboardShortcutPrompt.Volume Down'), ['VOLUME_DOWN']], [t('KeyboardShortcutPrompt.Take Screenshot'), ['TAKE_SCREENSHOT']], [t('KeyboardShortcutPrompt.Stats'), ['STATS']], + [t('KeyboardShortcutPrompt.Unskip Sponsor Block'), ['UNSKIP_SPONSOR_BLOCK']], [t('KeyboardShortcutPrompt.Play'), ['PLAY']], [t('KeyboardShortcutPrompt.Large Rewind'), ['LARGE_REWIND']], diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 2bab1f669915e..facea4c937d4b 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -2322,7 +2322,7 @@ export default defineComponent({ } switch (event.key.toLowerCase()) { - case 'enter': { + case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.UNSKIP_SPONSOR_BLOCK: { if (skippedSponsorBlockSegments.value.length > 0) { event.preventDefault() const segment = skippedSponsorBlockSegments.value[skippedSponsorBlockSegments.value.length - 1] diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 5362bac3477f1..2d28872793ecd 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -1205,3 +1205,4 @@ KeyboardShortcutPrompt: End: Seek to the end of the video Skip to Next Video: Skip to the next video in a playlist or next recommended video Skip to Previous Video: Skip to the previous video in a playlist + Unskip Sponsor Block: Unskip sponsor block segment From 03493b9761ce38ea53131a1afa176949dc54cf6d Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Wed, 13 May 2026 17:38:30 -0500 Subject: [PATCH 5/6] Add timer showing segment time remaining --- .../ft-shaka-video-player/ft-shaka-video-player.js | 11 +++++++++++ .../ft-shaka-video-player/ft-shaka-video-player.vue | 4 ++-- static/locales/en-US.yaml | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index facea4c937d4b..2cb1a08e5830b 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -577,6 +577,7 @@ export default defineComponent({ clearInterval(existingSkip.intervalId) existingSkip.timeLeft = 4 + existingSkip.segmentTimeLeft = 0 existingSkip.intervalId = setInterval(() => { existingSkip.timeLeft-- }, 1000) @@ -589,6 +590,7 @@ export default defineComponent({ endTime, unskipped: false, timeLeft: 4, + segmentTimeLeft: 0, intervalId: null, timeoutId: null } @@ -1257,6 +1259,14 @@ export default defineComponent({ if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) { skipSponsorBlockSegments(currentTime) } + + if (useSponsorBlock.value) { + for (const segment of skippedSponsorBlockSegments.value) { + if (segment.unskipped) { + segment.segmentTimeLeft = Math.max(0, segment.endTime - currentTime) + } + } + } } } @@ -2339,6 +2349,7 @@ export default defineComponent({ clearTimeout(segment.timeoutId) clearInterval(segment.intervalId) segment.timeLeft = null + segment.segmentTimeLeft = Math.max(0, segment.endTime - video_.currentTime) ignoredSponsorBlockSegments.add(segment.uuid) } } diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue index bc5f7b321a737..a45fafa327682 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue @@ -116,14 +116,14 @@ class="skippedSegmentsWrapper" >

{{ $t('Video.Player.Skipped segment', { segmentCategory: translatedCategory }) }}
- {{ unskipped ? $t('Video.Player.Press Enter to reskip') : $t('Video.Player.Press Enter to unskip', { timeLeft }) }} + {{ unskipped ? $t('Video.Player.Press Enter to reskip', { timeLeft: Math.ceil(segmentTimeLeft) }) : $t('Video.Player.Press Enter to unskip', { timeLeft }) }}

diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 2d28872793ecd..907aba04783d9 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -938,7 +938,7 @@ Video: Playback will resume automatically when your connection comes back: Playback will resume automatically when your connection comes back. Skipped segment: 'Skipped {segmentCategory} segment' Press Enter to unskip: 'Press Enter to unskip ({timeLeft}s)' - Press Enter to reskip: 'Press Enter to reskip' + Press Enter to reskip: 'Press Enter to reskip ({timeLeft}s remaining)' Watch: 'Remaining preroll-ad time: {remindingTimeSeconds}s': 'Remaining preroll-ad time: {remindingTimeSeconds}s' 'Remaining SABR backoff time: {remindingTimeSeconds}s': 'Remaining SABR backoff time: {remindingTimeSeconds}s' From 8fa01631194edecc9dbb5ae327d05994e0783cf4 Mon Sep 17 00:00:00 2001 From: Pillowytuba Date: Tue, 19 May 2026 20:03:41 -0500 Subject: [PATCH 6/6] Keep unskip available after reskipping SponsorBlock segment --- .../ft-shaka-video-player.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 2cb1a08e5830b..3db6f6b494480 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -2338,10 +2338,21 @@ export default defineComponent({ const segment = skippedSponsorBlockSegments.value[skippedSponsorBlockSegments.value.length - 1] if (segment.unskipped) { video_.currentTime = segment.endTime - const index = skippedSponsorBlockSegments.value.indexOf(segment) - if (index !== -1) { - skippedSponsorBlockSegments.value.splice(index, 1) - } + segment.unskipped = false + segment.timeLeft = 4 + segment.segmentTimeLeft = 0 + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + segment.intervalId = setInterval(() => { + segment.timeLeft-- + }, 1000) + segment.timeoutId = setTimeout(() => { + const index = skippedSponsorBlockSegments.value.indexOf(segment) + if (index !== -1) { + skippedSponsorBlockSegments.value.splice(index, 1) + } + ignoredSponsorBlockSegments.delete(segment.uuid) + }, 4000) ignoredSponsorBlockSegments.delete(segment.uuid) } else { video_.currentTime = segment.startTime