Add dearrow support for thumbnails (#4520)

* Add dearrow support for thumbnails

* add translations

* add missing tooltip

* make hidden thumbnails take a higher priority over dearrowed thumbnails

* Implement code suggestions

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
This commit is contained in:
ChunkyProgrammer 2024-01-14 23:20:15 -05:00 committed by GitHub
parent 95edf3b377
commit ae7a2fa221
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 11 deletions

View File

@ -8,9 +8,11 @@ import {
openExternalLink,
showToast,
toLocalePublicationString,
toDistractionFreeTitle
toDistractionFreeTitle,
deepCopy
} from '../../helpers/utils'
import { deArrowData } from '../../helpers/sponsorblock'
import { deArrowData, deArrowThumbnail } from '../../helpers/sponsorblock'
import debounce from 'lodash.debounce'
export default defineComponent({
name: 'FtListVideo',
@ -99,6 +101,7 @@ export default defineComponent({
isPremium: false,
hideViews: false,
addToPlaylistPromptCloseCallback: null,
debounceGetDeArrowThumbnail: null,
}
},
computed: {
@ -303,6 +306,14 @@ export default defineComponent({
},
thumbnail: function () {
if (this.thumbnailPreference === 'hidden') {
return require('../../assets/img/thumbnail_placeholder.svg')
}
if (this.useDeArrowThumbnails && this.deArrowCache?.thumbnail != null) {
return this.deArrowCache.thumbnail
}
let baseUrl
if (this.backendPreference === 'invidious') {
baseUrl = this.currentInvidiousInstance
@ -317,8 +328,6 @@ export default defineComponent({
return `${baseUrl}/vi/${this.id}/mq2.jpg`
case 'end':
return `${baseUrl}/vi/${this.id}/mq3.jpg`
case 'hidden':
return require('../../assets/img/thumbnail_placeholder.svg')
default:
return `${baseUrl}/vi/${this.id}/mqdefault.jpg`
}
@ -367,6 +376,13 @@ export default defineComponent({
}
},
displayDuration: function () {
if (this.useDeArrowTitles && (this.duration === '' || this.duration === '0:00') && this.deArrowCache?.videoDuration) {
return formatDurationAsTimestamp(this.deArrowCache.videoDuration)
}
return this.duration
},
playlistIdTypePairFinal() {
if (this.playlistId) {
return {
@ -424,6 +440,10 @@ export default defineComponent({
return this.$store.getters.getUseDeArrowTitles
},
useDeArrowThumbnails: function () {
return this.$store.getters.getUseDeArrowThumbnails
},
deArrowCache: function () {
return this.$store.getters.getDeArrowCache[this.id]
},
@ -444,21 +464,54 @@ export default defineComponent({
this.parseVideoData()
this.checkIfWatched()
if (this.useDeArrowTitles && !this.deArrowCache) {
if ((this.useDeArrowTitles || this.useDeArrowThumbnails) && !this.deArrowCache) {
this.fetchDeArrowData()
}
if (this.useDeArrowThumbnails && this.deArrowCache && this.deArrowCache.thumbnail == null) {
if (this.debounceGetDeArrowThumbnail == null) {
this.debounceGetDeArrowThumbnail = debounce(this.fetchDeArrowThumbnail, 1000)
}
this.debounceGetDeArrowThumbnail()
}
},
methods: {
fetchDeArrowThumbnail: async function() {
if (this.thumbnailPreference === 'hidden') { return }
const videoId = this.id
const thumbnail = await deArrowThumbnail(videoId, this.deArrowCache.thumbnailTimestamp)
if (thumbnail) {
const deArrowCacheClone = deepCopy(this.deArrowCache)
deArrowCacheClone.thumbnail = thumbnail
this.$store.commit('addThumbnailToDeArrowCache', deArrowCacheClone)
}
},
fetchDeArrowData: async function() {
const videoId = this.id
const data = await deArrowData(this.id)
const cacheData = { videoId, title: null }
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)) {
cacheData.title = data.titles[0].title
}
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
} else if (data?.videoDuration != null) {
cacheData.thumbnailTimestamp = data.videoDuration * data.randomTime
}
cacheData.videoDuration = data?.videoDuration ? Math.floor(data.videoDuration) : null
// Save data to cache whether data available or not to prevent duplicate requests
this.$store.commit('addVideoToDeArrowCache', cacheData)
// fetch dearrow thumbnails if enabled
if (this.useDeArrowThumbnails && this.deArrowCache?.thumbnail === null) {
if (this.debounceGetDeArrowThumbnail == null) {
this.debounceGetDeArrowThumbnail = debounce(this.fetchDeArrowThumbnail, 1000)
}
this.debounceGetDeArrowThumbnail()
}
},
handleExternalPlayer: function () {

View File

@ -24,14 +24,14 @@
>
</router-link>
<div
v-if="isLive || isUpcoming || (duration !== '' && duration !== '0:00')"
v-if="isLive || isUpcoming || (displayDuration !== '' && displayDuration !== '0:00')"
class="videoDuration"
:class="{
live: isLive,
upcoming: isUpcoming
}"
>
{{ isLive ? $t("Video.Live") : (isUpcoming ? $t("Video.Upcoming") : duration) }}
{{ isLive ? $t("Video.Live") : (isUpcoming ? $t("Video.Upcoming") : displayDuration) }}
</div>
<ft-icon-button
v-if="externalPlayer !== ''"

View File

@ -42,7 +42,13 @@ export default defineComponent({
useDeArrowTitles: function () {
return this.$store.getters.getUseDeArrowTitles
}
},
useDeArrowThumbnails: function () {
return this.$store.getters.getUseDeArrowThumbnails
},
deArrowThumbnailGeneratorUrl: function () {
return this.$store.getters.getDeArrowThumbnailGeneratorUrl
},
},
methods: {
handleUpdateSponsorBlock: function (value) {
@ -53,12 +59,22 @@ export default defineComponent({
this.updateUseDeArrowTitles(value)
},
handleUpdateUseDeArrowThumbnails: function (value) {
this.updateUseDeArrowThumbnails(value)
},
handleUpdateSponsorBlockUrl: function (value) {
const sponsorBlockUrlWithoutTrailingSlash = value.replace(/\/$/, '')
const sponsorBlockUrlWithoutApiSuffix = sponsorBlockUrlWithoutTrailingSlash.replace(/\/api$/, '')
this.updateSponsorBlockUrl(sponsorBlockUrlWithoutApiSuffix)
},
handleUpdateDeArrowThumbnailGeneratorUrl: function (value) {
const urlWithoutTrailingSlash = value.replace(/\/$/, '')
const urlWithoutApiSuffix = urlWithoutTrailingSlash.replace(/\/api$/, '')
this.updateDeArrowThumbnailGeneratorUrl(urlWithoutApiSuffix)
},
handleUpdateSponsorBlockShowSkippedToast: function (value) {
this.updateSponsorBlockShowSkippedToast(value)
},
@ -67,7 +83,9 @@ export default defineComponent({
'updateUseSponsorBlock',
'updateSponsorBlockUrl',
'updateSponsorBlockShowSkippedToast',
'updateUseDeArrowTitles'
'updateUseDeArrowTitles',
'updateUseDeArrowThumbnails',
'updateDeArrowThumbnailGeneratorUrl'
])
}
})

View File

@ -14,9 +14,15 @@
:tooltip="$t('Tooltips.SponsorBlock Settings.UseDeArrowTitles')"
@change="handleUpdateUseDeArrowTitles"
/>
<ft-toggle-switch
:label="$t('Settings.SponsorBlock Settings.UseDeArrowThumbnails')"
:default-value="useDeArrowThumbnails"
:tooltip="$t('Tooltips.SponsorBlock Settings.UseDeArrowThumbnails')"
@change="handleUpdateUseDeArrowThumbnails"
/>
</ft-flex-box>
<template
v-if="useSponsorBlock || useDeArrowTitles"
v-if="useSponsorBlock || useDeArrowTitles || useDeArrowThumbnails"
>
<ft-flex-box
v-if="useSponsorBlock"
@ -37,6 +43,19 @@
@input="handleUpdateSponsorBlockUrl"
/>
</ft-flex-box>
<ft-flex-box
v-if="useDeArrowThumbnails"
>
<ft-input
v-if="useDeArrowThumbnails"
:placeholder="$t('Settings.SponsorBlock Settings[\'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)\']')"
:show-action-button="false"
:show-label="true"
:value="deArrowThumbnailGeneratorUrl"
@input="handleUpdateDeArrowThumbnailGeneratorUrl"
/>
</ft-flex-box>
<ft-flex-box
v-if="useSponsorBlock"
>

View File

@ -56,3 +56,31 @@ export async function deArrowData(videoId) {
throw error
}
}
export async function deArrowThumbnail(videoId, timestamp) {
let requestUrl = `${store.getters.getDeArrowThumbnailGeneratorUrl}/api/v1/getThumbnail?videoID=` + videoId
if (timestamp != null) {
requestUrl += `&time=${timestamp}`
}
try {
const response = await fetch(requestUrl)
// 404 means that there are no thumbnails found for the video
if (response.status === 404) {
return undefined
}
if (response.ok) {
return response.url
}
// this usually means that a thumbnail was not generated on the server yet so we'll log the error but otherwise ignore it.
const json = await response.json()
console.error(json)
return undefined
} catch (error) {
console.error('failed to fetch DeArrow data', requestUrl, error)
throw error
}
}

View File

@ -300,6 +300,8 @@ const state = {
allowDashAv1Formats: false,
commentAutoLoadEnabled: false,
useDeArrowTitles: false,
useDeArrowThumbnails: false,
deArrowThumbnailGeneratorUrl: 'https://dearrow-thumb.ajay.app'
}
const stateWithSideEffects = {

View File

@ -782,6 +782,10 @@ const mutations = {
}
},
addThumbnailToDeArrowCache (state, payload) {
vueSet(state.deArrowCache, payload.videoId, payload)
},
addToSessionSearchHistory (state, payload) {
const sameSearch = state.sessionSearchHistory.findIndex((search) => {
return search.query === payload.query && searchFiltersMatch(payload.searchSettings, search.searchSettings)

View File

@ -534,6 +534,8 @@ Settings:
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock API Url (Default is https://sponsor.ajay.app)
Notify when sponsor segment is skipped: Notify when sponsor segment is skipped
UseDeArrowTitles: Use DeArrow Video Titles
UseDeArrowThumbnails: Use DeArrow for thumbnails
'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
Auto Skip: Auto Skip
@ -984,6 +986,7 @@ Tooltips:
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:
UseDeArrowTitles: Replace video titles with user-submitted titles from DeArrow.
UseDeArrowThumbnails: Replace video thumbnails with thumbnails from DeArrow.
# Toast Messages
Local API Error (Click to copy): Local API Error (Click to copy)