mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 13:31:32 +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 { retrieveVideo, retrieveVideoContent } from '@renderer/services/ApiService'
|
||||
import { retrieveVideo } from '@renderer/services/ApiService'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
@ -13,6 +13,8 @@ import { Video } from '@renderer/types/video'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useRetrieveThumbnail } from './useRetrieveThumbnail'
|
||||
|
||||
const logger = loggerService.withContext('useVideo')
|
||||
|
||||
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)
|
||||
}
|
||||
const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
|
||||
const retrieveThumbnail = useRetrieveThumbnail()
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
logger.error('Failed to fetch video status updates', error)
|
||||
@ -110,26 +113,33 @@ export const useVideos = (providerId: string) => {
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'completed':
|
||||
// Only update it when in_progress/queued -> completed
|
||||
case 'completed': {
|
||||
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
|
||||
setVideo({ ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo })
|
||||
// try to request thumbnail here.
|
||||
retrieveVideoContent({
|
||||
type: 'openai',
|
||||
provider,
|
||||
videoId: retrievedVideo.id,
|
||||
query: { variant: 'thumbnail' }
|
||||
})
|
||||
.then((v) => {
|
||||
// TODO: this is a iamge/webp type response. save it somewhere.
|
||||
logger.debug('thumbnail resposne', v.response)
|
||||
const newVideo = { ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo } as const
|
||||
setVideo(newVideo)
|
||||
// Try to get thumbnail
|
||||
retrieveThumbnail(newVideo)
|
||||
.then((thumbnail) => {
|
||||
const latestVideo = videosRef.current?.find((v) => v.id === newVideo.id)
|
||||
if (
|
||||
thumbnail !== null &&
|
||||
latestVideo &&
|
||||
latestVideo.status !== 'queued' &&
|
||||
latestVideo.status !== 'in_progress' &&
|
||||
latestVideo.status !== 'failed'
|
||||
) {
|
||||
setVideo({
|
||||
...latestVideo,
|
||||
thumbnail
|
||||
})
|
||||
}
|
||||
})
|
||||
.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
|
||||
}
|
||||
case 'failed':
|
||||
setVideo({
|
||||
...storeVideo,
|
||||
|
||||
@ -41,18 +41,20 @@ export interface VideoInProgress extends VideoBase {
|
||||
}
|
||||
export interface VideoCompleted extends VideoBase {
|
||||
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
|
||||
}
|
||||
|
||||
export interface VideoDownloading extends VideoBase {
|
||||
readonly status: 'downloading'
|
||||
/** Base64 image string */
|
||||
thumbnail: string | null
|
||||
/** integer percent */
|
||||
progress: number
|
||||
}
|
||||
export interface VideoDownloaded extends VideoBase {
|
||||
readonly status: 'downloaded'
|
||||
/** Base64 image string */
|
||||
thumbnail: string | null
|
||||
/** Managed by fileManager */
|
||||
fileId: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user