mirror of https://github.com/FreeTubeApp/FreeTube
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:
parent
95edf3b377
commit
ae7a2fa221
|
@ -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 () {
|
||||
|
|
|
@ -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 !== ''"
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,6 +300,8 @@ const state = {
|
|||
allowDashAv1Formats: false,
|
||||
commentAutoLoadEnabled: false,
|
||||
useDeArrowTitles: false,
|
||||
useDeArrowThumbnails: false,
|
||||
deArrowThumbnailGeneratorUrl: 'https://dearrow-thumb.ajay.app'
|
||||
}
|
||||
|
||||
const stateWithSideEffects = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue