Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/renderer/components/SponsorBlockSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
:tooltip="$t('Tooltips.SponsorBlock Settings.UseDeArrowThumbnails')"
@change="handleUpdateUseDeArrowThumbnails"
/>
<FtToggleSwitch
v-if="useDeArrowTitles || useDeArrowThumbnails"
:label="$t('Settings.SponsorBlock Settings.DeArrowCasualMode')"
:default-value="deArrowCasualMode"
:tooltip="$t('Tooltips.SponsorBlock Settings.DeArrowCasualMode')"
@change="handleUpdateDeArrowCasualMode"
/>
</FtFlexBox>
<template
v-if="useSponsorBlock || useDeArrowTitles || useDeArrowThumbnails"
Expand Down Expand Up @@ -106,6 +113,9 @@ const useDeArrowTitles = computed(() => store.getters.getUseDeArrowTitles)
/** @type {import('vue').ComputedRef<boolean>} */
const useDeArrowThumbnails = computed(() => store.getters.getUseDeArrowThumbnails)

/** @type {import('vue').ComputedRef<boolean>} */
const deArrowCasualMode = computed(() => store.getters.getDeArrowCasualMode)

/** @type {import('vue').ComputedRef<string>} */
const deArrowThumbnailGeneratorUrl = computed(() => store.getters.getDeArrowThumbnailGeneratorUrl)

Expand All @@ -130,6 +140,13 @@ function handleUpdateUseDeArrowThumbnails(value) {
store.dispatch('updateUseDeArrowThumbnails', value)
}

/**
* @param {boolean} value
*/
function handleUpdateDeArrowCasualMode(value) {
store.dispatch('updateDeArrowCasualMode', value)
}

/**
* @param {boolean} value
*/
Expand Down
38 changes: 35 additions & 3 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,9 @@ export default defineComponent({
useDeArrowThumbnails: function () {
return this.$store.getters.getUseDeArrowThumbnails
},
deArrowCasualMode: function () {
return this.$store.getters.getDeArrowCasualMode
},
deArrowChangedContent: function () {
return (this.useDeArrowThumbnails && this.deArrowCache?.thumbnail) ||
(this.useDeArrowTitles && this.deArrowCache?.title &&
Expand Down Expand Up @@ -598,9 +601,13 @@ export default defineComponent({
const videoId = this.id
const data = await deArrowData(this.id)
const cacheData = { videoId, title: null, videoDuration: null, thumbnail: null, thumbnailTimestamp: null }
if (Array.isArray(data?.titles) && data.titles.length > 0 && (data.titles[0].locked || data.titles[0].votes >= 0)) {
// remove dearrow formatting markers https://github.com/ajayyy/DeArrow/blob/0da266485be902fe54259214c3cd7c942f2357c5/src/titles/titleFormatter.ts#L460
cacheData.title = data.titles[0].title.replaceAll(/(^|\s)>(\S)/g, '$1$2').trim()
if (Array.isArray(data?.titles) && data.titles.length > 0) {
const selectedTitle = this.selectDeArrowTitle(data.titles, data.casualMode)
if (selectedTitle) {
// remove dearrow formatting markers
// https://github.com/ajayyy/DeArrow/blob/0da266485be902fe54259214c3cd7c942f2357c5/src/titles/titleFormatter.ts#L460
cacheData.title = selectedTitle.replaceAll(/(^|\s)>(\S)/g, '$1$2').trim()
}
}
if (Array.isArray(data?.thumbnails) && data.thumbnails.length > 0 && (data.thumbnails[0].locked || data.thumbnails[0].votes >= 0)) {
cacheData.thumbnailTimestamp = data.thumbnails.at(0).timestamp
Expand All @@ -621,6 +628,31 @@ export default defineComponent({
this.debounceGetDeArrowThumbnail()
}
},
selectDeArrowTitle: function(titles, casualMode) {
if (!Array.isArray(titles) || titles.length === 0) {
return null
}

if (casualMode) {
// Prefer a community-approved original title in casual mode.
const goodOriginal = titles.find(
(t) => t.original && (t.locked || t.votes >= 0)
)
if (goodOriginal) {
return goodOriginal.title
}

// No well-voted original — use the best custom title instead.
const bestCustom = titles.find(
(t) => !t.original && (t.locked || t.votes >= 0)
)
return bestCustom ? bestCustom.title : null
}

// Classic mode: use the first (highest-quality) title if it is trusted.
const best = titles[0]
return (best.locked || best.votes >= 0) ? best.title : null
},
toggleDeArrow() {
if (!this.deArrowChangedContent) {
return
Expand Down
22 changes: 18 additions & 4 deletions src/renderer/helpers/sponsorblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import store from '../store/index'

async function getVideoHash(videoId) {
const videoIdBuffer = new TextEncoder().encode(videoId)

const hashBuffer = await crypto.subtle.digest('SHA-256', videoIdBuffer)
const hashArray = new Uint8Array(hashBuffer)

return hashArray[0].toString(16).padStart(2, '0') +
hashArray[1].toString(16).padStart(2, '0')
}
Expand Down Expand Up @@ -60,7 +58,14 @@ export async function sponsorBlockSkipSegments(videoId, categories) {

export async function deArrowData(videoId) {
const videoIdHashPrefix = await getVideoHash(videoId)
const requestUrl = `${store.getters.getSponsorBlockUrl}/api/branding/${videoIdHashPrefix}`
const deArrowCasualMode = store.getters.getDeArrowCasualMode

// When casual mode is enabled we request all titles (including those with
// negative scores) so that upvoted original titles are also included in the
// response. The server already returns data in quality order, so we can
// still rely on position when picking the best non-original title as a
// fallback.
const requestUrl = `${store.getters.getSponsorBlockUrl}/api/branding/${videoIdHashPrefix}${deArrowCasualMode ? '?fetchAll=true' : ''}`

try {
const response = await fetch(requestUrl)
Expand All @@ -71,7 +76,15 @@ export async function deArrowData(videoId) {
}

const json = await response.json()
return json[videoId] ?? undefined
const videoData = json[videoId] ?? undefined

if (videoData === undefined) {
return undefined
}

// Attach the current mode so the consumer can make the right selection
// decision without having to reach back into the store.
return { ...videoData, casualMode: deArrowCasualMode }
} catch (error) {
console.error('failed to fetch DeArrow data', requestUrl, error)
throw error
Expand All @@ -80,6 +93,7 @@ export async function deArrowData(videoId) {

export async function deArrowThumbnail(videoId, timestamp) {
let requestUrl = `${store.getters.getDeArrowThumbnailGeneratorUrl}/api/v1/getThumbnail?videoID=` + videoId

if (timestamp != null) {
requestUrl += `&time=${timestamp}`
}
Expand Down
1 change: 1 addition & 0 deletions src/renderer/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ const state = {
settingsPassword: '',
useDeArrowTitles: false,
useDeArrowThumbnails: false,
deArrowCasualMode: false,
deArrowThumbnailGeneratorUrl: 'https://dearrow-thumb.ajay.app',
// This makes the `favorites` playlist uses as quick bookmark target
// If the playlist is removed quick bookmark is disabled
Expand Down
2 changes: 2 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ Settings:
Notify when sponsor segment is skipped: Notify when sponsor segment is skipped
UseDeArrowTitles: Use DeArrow Video Titles
UseDeArrowThumbnails: Use DeArrow for thumbnails
DeArrowCasualMode: Use DeArrow Casual Mode
'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)': 'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)'
Skip Options:
Skip Option: Skip Option
Expand Down Expand Up @@ -1093,6 +1094,7 @@ Tooltips:
SponsorBlock Settings:
UseDeArrowTitles: Replace video titles with user-submitted titles from DeArrow.
UseDeArrowThumbnails: Replace video thumbnails with thumbnails from DeArrow.
DeArrowCasualMode: In Casual mode, original titles that the community has upvoted will be kept. Titles that are genuinely misleading will still be replaced.

# Toast Messages
Local API Error (Click to copy): Local API Error (Click to copy)
Expand Down