mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 05:51:26 +08:00
feat(video): add thumbnail retrieval hook and update video interfaces
Implement useRetrieveThumbnail hook to handle thumbnail fetching and caching Update video interfaces to clarify thumbnail field types and add missing documentation Refactor useVideos to use new thumbnail hook instead of direct API calls
This commit is contained in:
parent
efd5e9dcf2
commit
15b7eb78c1
70
src/renderer/src/hooks/video/useRetrieveThumbnail.ts
Normal file
70
src/renderer/src/hooks/video/useRetrieveThumbnail.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { retrieveVideoContent } from '@renderer/services/ApiService'
|
||||||
|
import ImageStorage from '@renderer/services/ImageStorage'
|
||||||
|
import { getProviderById } from '@renderer/services/ProviderService'
|
||||||
|
import { Video } from '@renderer/types'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('useRetrieveThumbnail')
|
||||||
|
|
||||||
|
const pendingSet = new Set<string>()
|
||||||
|
|
||||||
|
export const useRetrieveThumbnail = () => {
|
||||||
|
const retrieveThumbnail = useCallback(async (video: Video): Promise<string | null> => {
|
||||||
|
const provider = getProviderById(video.providerId)
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Provider not found for id ${video.providerId}`)
|
||||||
|
}
|
||||||
|
const thumbnailKey = `video-thumbnail-${video.id}`
|
||||||
|
if (pendingSet.has(thumbnailKey)) {
|
||||||
|
throw new Error('Thumbnail retrieval already pending')
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSet.add(thumbnailKey)
|
||||||
|
try {
|
||||||
|
const cachedThumbnail = await ImageStorage.get(thumbnailKey)
|
||||||
|
if (cachedThumbnail) {
|
||||||
|
return cachedThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await retrieveVideoContent({
|
||||||
|
type: 'openai',
|
||||||
|
provider,
|
||||||
|
videoId: video.id,
|
||||||
|
query: { variant: 'thumbnail' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { response } = result
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unexpected thumbnail status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
if (!blob || blob.size === 0) {
|
||||||
|
throw new Error('Thumbnail response body is empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64 = await new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
resolve(reader.result)
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to convert thumbnail to base64'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = () => reject(reader.error ?? new Error('Failed to read thumbnail blob'))
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
|
||||||
|
await ImageStorage.set(thumbnailKey, base64)
|
||||||
|
return base64
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to get thumbnail for video ${video.id}`, e as Error)
|
||||||
|
} finally {
|
||||||
|
pendingSet.delete(thumbnailKey)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [])
|
||||||
|
return retrieveThumbnail
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { retrieveVideo, retrieveVideoContent } from '@renderer/services/ApiService'
|
import { retrieveVideo } from '@renderer/services/ApiService'
|
||||||
import { getProviderById } from '@renderer/services/ProviderService'
|
import { getProviderById } from '@renderer/services/ProviderService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
@ -13,6 +13,8 @@ import { Video } from '@renderer/types/video'
|
|||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
import { useRetrieveThumbnail } from './useRetrieveThumbnail'
|
||||||
|
|
||||||
const logger = loggerService.withContext('useVideo')
|
const logger = loggerService.withContext('useVideo')
|
||||||
|
|
||||||
export const useVideos = (providerId: string) => {
|
export const useVideos = (providerId: string) => {
|
||||||
@ -80,6 +82,7 @@ export const useVideos = (providerId: string) => {
|
|||||||
return result.filter((p) => p.status === 'fulfilled').map((p) => p.value)
|
return result.filter((p) => p.status === 'fulfilled').map((p) => p.value)
|
||||||
}
|
}
|
||||||
const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
|
const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
|
||||||
|
const retrieveThumbnail = useRetrieveThumbnail()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to fetch video status updates', error)
|
logger.error('Failed to fetch video status updates', error)
|
||||||
@ -110,26 +113,33 @@ export const useVideos = (providerId: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'completed':
|
case 'completed': {
|
||||||
// Only update it when in_progress/queued -> completed
|
|
||||||
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
|
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
|
||||||
setVideo({ ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo })
|
const newVideo = { ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo } as const
|
||||||
// try to request thumbnail here.
|
setVideo(newVideo)
|
||||||
retrieveVideoContent({
|
// Try to get thumbnail
|
||||||
type: 'openai',
|
retrieveThumbnail(newVideo)
|
||||||
provider,
|
.then((thumbnail) => {
|
||||||
videoId: retrievedVideo.id,
|
const latestVideo = videosRef.current?.find((v) => v.id === newVideo.id)
|
||||||
query: { variant: 'thumbnail' }
|
if (
|
||||||
})
|
thumbnail !== null &&
|
||||||
.then((v) => {
|
latestVideo &&
|
||||||
// TODO: this is a iamge/webp type response. save it somewhere.
|
latestVideo.status !== 'queued' &&
|
||||||
logger.debug('thumbnail resposne', v.response)
|
latestVideo.status !== 'in_progress' &&
|
||||||
|
latestVideo.status !== 'failed'
|
||||||
|
) {
|
||||||
|
setVideo({
|
||||||
|
...latestVideo,
|
||||||
|
thumbnail
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error(`Failed to get thumbnail for video ${retrievedVideo.id}`, e as Error)
|
logger.error('Failed to get thumbnail', e as Error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'failed':
|
case 'failed':
|
||||||
setVideo({
|
setVideo({
|
||||||
...storeVideo,
|
...storeVideo,
|
||||||
|
|||||||
@ -41,18 +41,20 @@ export interface VideoInProgress extends VideoBase {
|
|||||||
}
|
}
|
||||||
export interface VideoCompleted extends VideoBase {
|
export interface VideoCompleted extends VideoBase {
|
||||||
readonly status: 'completed'
|
readonly status: 'completed'
|
||||||
/** When generation completed, firstly try to retrieve thumbnail. */
|
/** Base64 image string. When generation completed, firstly try to retrieve thumbnail. */
|
||||||
thumbnail: string | null
|
thumbnail: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDownloading extends VideoBase {
|
export interface VideoDownloading extends VideoBase {
|
||||||
readonly status: 'downloading'
|
readonly status: 'downloading'
|
||||||
|
/** Base64 image string */
|
||||||
thumbnail: string | null
|
thumbnail: string | null
|
||||||
/** integer percent */
|
/** integer percent */
|
||||||
progress: number
|
progress: number
|
||||||
}
|
}
|
||||||
export interface VideoDownloaded extends VideoBase {
|
export interface VideoDownloaded extends VideoBase {
|
||||||
readonly status: 'downloaded'
|
readonly status: 'downloaded'
|
||||||
|
/** Base64 image string */
|
||||||
thumbnail: string | null
|
thumbnail: string | null
|
||||||
/** Managed by fileManager */
|
/** Managed by fileManager */
|
||||||
fileId: string
|
fileId: string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user