diff --git a/src/renderer/src/hooks/video/useProviderVideos.ts b/src/renderer/src/hooks/video/useProviderVideos.ts new file mode 100644 index 0000000000..8e92c84aa7 --- /dev/null +++ b/src/renderer/src/hooks/video/useProviderVideos.ts @@ -0,0 +1,158 @@ +import { loggerService } from '@logger' +import { retrieveVideo } from '@renderer/services/ApiService' +import { getProviderById } from '@renderer/services/ProviderService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { addVideoAction, setVideoAction, setVideosAction, updateVideoAction } from '@renderer/store/video' +import { Video } from '@renderer/types/video' +import { useCallback, useEffect, useRef } from 'react' +import useSWR from 'swr' + +import { useVideos } from './useVideos' +import { useVideoThumbnail } from './useVideoThumbnail' + +const logger = loggerService.withContext('useVideo') + +export const useProviderVideos = (providerId: string) => { + const { removeVideo } = useVideos() + const videos = useAppSelector((state) => state.video.videoMap[providerId]) + const videosRef = useRef(videos) + const dispatch = useAppDispatch() + + useEffect(() => { + videosRef.current = videos + }, [videos]) + + const addVideo = useCallback( + (video: Video) => { + if (videos && videos.every((v) => v.id !== video.id)) { + dispatch(addVideoAction({ providerId, video })) + } + }, + [dispatch, providerId, videos] + ) + + const updateVideo = useCallback( + (update: Partial> & { id: string }) => { + dispatch(updateVideoAction({ providerId, update })) + }, + [dispatch, providerId] + ) + + const setVideo = useCallback( + (video: Video) => { + dispatch(setVideoAction({ providerId, video })) + }, + [dispatch, providerId] + ) + + const setVideos = useCallback( + (newVideos: Video[]) => { + dispatch(setVideosAction({ providerId, videos: newVideos })) + }, + [dispatch, providerId] + ) + + const removeProviderVideo = useCallback( + (videoId: string) => { + removeVideo(videoId, providerId) + }, + [providerId, removeVideo] + ) + + useEffect(() => { + if (!videos) { + setVideos([]) + } + }, [setVideos, videos]) + + // update videos from api + // NOTE: This provider should support openai videos endpoint. No runtime check here. + const provider = getProviderById(providerId) + const fetcher = async () => { + if (!videos || !provider) return [] + const openaiVideos = videos + .filter((v) => v.type === 'openai') + .filter((v) => v.status === 'queued' || v.status === 'in_progress') + const jobs = openaiVideos.map((v) => retrieveVideo({ type: 'openai', videoId: v.id, provider })) + const result = await Promise.allSettled(jobs) + return result.filter((p) => p.status === 'fulfilled').map((p) => p.value) + } + const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 }) + const { retrieveThumbnail } = useVideoThumbnail() + useEffect(() => { + if (error) { + logger.error('Failed to fetch video status updates', error) + return + } + if (!provider) { + logger.warn(`Provider ${providerId} not found.`) + return + } + const videos = videosRef.current + + if (!data || !videos) return + data.forEach((v) => { + const retrievedVideo = v.video + const storeVideo = videos.find((v) => v.id === retrievedVideo.id) + if (!storeVideo) { + logger.warn(`Try to update video ${retrievedVideo.id}, but it's not in the store.`) + return + } + switch (retrievedVideo.status) { + case 'in_progress': + if (storeVideo.status === 'queued' || storeVideo.status === 'in_progress') { + setVideo({ + ...storeVideo, + status: 'in_progress', + progress: retrievedVideo.progress, + metadata: retrievedVideo + }) + } + break + case 'completed': { + if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') { + 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', e as Error) + }) + } + break + } + case 'failed': + setVideo({ + ...storeVideo, + status: 'failed', + error: retrievedVideo.error, + metadata: retrievedVideo + }) + } + }) + }, [data, error, provider, providerId, retrieveThumbnail, setVideo]) + + return { + videos: videos ?? [], + addVideo, + updateVideo, + setVideos, + setVideo, + removeVideo: removeProviderVideo + } +} diff --git a/src/renderer/src/hooks/video/useVideo.ts b/src/renderer/src/hooks/video/useVideo.ts index 378746158f..c28a7d50b4 100644 --- a/src/renderer/src/hooks/video/useVideo.ts +++ b/src/renderer/src/hooks/video/useVideo.ts @@ -1,7 +1,7 @@ -import { useVideos } from './useVideos' +import { useProviderVideos } from './useProviderVideos' export const useVideo = (providerId: string, id: string) => { - const { videos } = useVideos(providerId) + const { videos } = useProviderVideos(providerId) const video = videos.find((v) => v.id === id) return video } diff --git a/src/renderer/src/hooks/video/useVideos.ts b/src/renderer/src/hooks/video/useVideos.ts index 88cebf0e94..a82840ccd1 100644 --- a/src/renderer/src/hooks/video/useVideos.ts +++ b/src/renderer/src/hooks/video/useVideos.ts @@ -1,162 +1,48 @@ -import { loggerService } from '@logger' -import { retrieveVideo } from '@renderer/services/ApiService' -import { getProviderById } from '@renderer/services/ProviderService' +import FileManager from '@renderer/services/FileManager' import { useAppDispatch, useAppSelector } from '@renderer/store' -import { - addVideoAction, - removeVideoAction, - setVideoAction, - setVideosAction, - updateVideoAction -} from '@renderer/store/video' -import { Video } from '@renderer/types/video' -import { useCallback, useEffect, useRef } from 'react' -import useSWR from 'swr' +import { removeVideoAction } from '@renderer/store/video' +import { objectValues } from '@renderer/types' +import { useCallback } from 'react' -import { useRetrieveThumbnail } from './useRetrieveThumbnail' +import { useVideoThumbnail } from './useVideoThumbnail' -const logger = loggerService.withContext('useVideo') - -export const useVideos = (providerId: string) => { - const videos = useAppSelector((state) => state.video.videoMap[providerId]) - const videosRef = useRef(videos) +export const useVideos = () => { + const videoMap = useAppSelector((state) => state.video.videoMap) const dispatch = useAppDispatch() - useEffect(() => { - videosRef.current = videos - }, [videos]) + const { removeThumbnail } = useVideoThumbnail() - const addVideo = useCallback( - (video: Video) => { - if (videos && videos.every((v) => v.id !== video.id)) { - dispatch(addVideoAction({ providerId, video })) - } - }, - [dispatch, providerId, videos] - ) + const videos = objectValues(videoMap) + .flat() + .filter((v) => v !== undefined) - const updateVideo = useCallback( - (update: Partial> & { id: string }) => { - dispatch(updateVideoAction({ providerId, update })) + const getVideo = useCallback( + (videoId: string) => { + return videos.find((v) => v.id === videoId) }, - [dispatch, providerId] - ) - - const setVideo = useCallback( - (video: Video) => { - dispatch(setVideoAction({ providerId, video })) - }, - [dispatch, providerId] - ) - - const setVideos = useCallback( - (newVideos: Video[]) => { - dispatch(setVideosAction({ providerId, videos: newVideos })) - }, - [dispatch, providerId] + [videos] ) const removeVideo = useCallback( - (videoId: string) => { - dispatch(removeVideoAction({ providerId, videoId })) - }, - [dispatch, providerId] - ) - - useEffect(() => { - if (!videos) { - setVideos([]) - } - }, [setVideos, videos]) - - // update videos from api - // NOTE: This provider should support openai videos endpoint. No runtime check here. - const provider = getProviderById(providerId) - const fetcher = async () => { - if (!videos || !provider) return [] - const openaiVideos = videos - .filter((v) => v.type === 'openai') - .filter((v) => v.status === 'queued' || v.status === 'in_progress') - const jobs = openaiVideos.map((v) => retrieveVideo({ type: 'openai', videoId: v.id, provider })) - const result = await Promise.allSettled(jobs) - 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) - return - } - if (!provider) { - logger.warn(`Provider ${providerId} not found.`) - return - } - const videos = videosRef.current - - if (!data || !videos) return - data.forEach((v) => { - const retrievedVideo = v.video - const storeVideo = videos.find((v) => v.id === retrievedVideo.id) - if (!storeVideo) { - logger.warn(`Try to update video ${retrievedVideo.id}, but it's not in the store.`) + (videoId: string, providerId?: string) => { + const video = getVideo(videoId) + if (!video) { return } - switch (retrievedVideo.status) { - case 'in_progress': - if (storeVideo.status === 'queued' || storeVideo.status === 'in_progress') { - setVideo({ - ...storeVideo, - status: 'in_progress', - progress: retrievedVideo.progress, - metadata: retrievedVideo - }) - } - break - case 'completed': { - if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') { - 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', e as Error) - }) - } - break - } - case 'failed': - setVideo({ - ...storeVideo, - status: 'failed', - error: retrievedVideo.error, - metadata: retrievedVideo - }) + if (!providerId) { + providerId = video.providerId } - }) - }, [data, error, provider, providerId, setVideo]) + // should delete from redux state, and related thumbnail image, video file + if (video.thumbnail) { + removeThumbnail(videoId) + } + if (video.fileId) { + FileManager.deleteFile(video.fileId) + } + dispatch(removeVideoAction({ providerId, videoId })) + }, + [dispatch, getVideo, removeThumbnail] + ) - return { - videos: videos ?? [], - addVideo, - updateVideo, - setVideos, - setVideo, - removeVideo - } + return { videos, getVideo, removeVideo } }