From 378c80cf3cd3e2810d8d8078ec35f972d68d8f5a Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 20 Jun 2025 20:18:32 +0300 Subject: [PATCH] added qortal multi publish status --- package.json | 3 +- .../MultiPublish/MultiPublishDialog.tsx | 380 ++++++++++++------ .../VideoPlayer/SubtitleManager.tsx | 12 - src/components/VideoPlayer/VideoControls.tsx | 3 - src/components/VideoPlayer/VideoPlayer.tsx | 89 +--- .../VideoPlayer/useVideoPlayerController.tsx | 1 - src/hooks/useGlobalPipPlayer.tsx | 5 +- src/hooks/usePublish.tsx | 65 ++- src/hooks/useResources.tsx | 7 - src/state/multiplePublish.ts | 84 +++- src/utils/base64.ts | 1 - tsup.config.ts | 3 +- 12 files changed, 393 insertions(+), 260 deletions(-) diff --git a/package.json b/package.json index 1458201..16d494f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qapp-core", - "version": "1.0.31", + "version": "1.0.32", "description": "Qortal's core React library with global state, UI components, and utilities", "main": "dist/index.js", "module": "dist/index.mjs", @@ -49,7 +49,6 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", - "mediainfo.js": "^0.3.5", "react": "^19.0.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.2" diff --git a/src/components/MultiPublish/MultiPublishDialog.tsx b/src/components/MultiPublish/MultiPublishDialog.tsx index 40eb47d..5e3c3f3 100644 --- a/src/components/MultiPublish/MultiPublishDialog.tsx +++ b/src/components/MultiPublish/MultiPublishDialog.tsx @@ -13,157 +13,307 @@ import { Typography, Box, LinearProgress, - Stack + Stack, + DialogActions, + Button } from '@mui/material'; import { PublishStatus, useMultiplePublishStore, usePublishStatusStore } from "../../state/multiplePublish"; import { ResourceToPublish } from "../../types/qortalRequests/types"; +import { QortalGetMetadata } from "../../types/interfaces/resources"; +import ErrorIcon from '@mui/icons-material/Error'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; - - - -const MultiPublishDialogComponent = () => { - const { resources, isPublishing } = useMultiplePublishStore((state) => ({ - resources: state.resources, - isPublishing: state.isPublishing -})); -const { publishStatus, setPublishStatusByKey, getPublishStatusByKey } = usePublishStatusStore(); - - - useEffect(() => { - function handleNavigation(event: any) { - if (event.data?.action === 'PUBLISH_STATUS') { - const data = event.data; - console.log('datadata', data) - // Validate required structure before continuing - if ( - !data.publishLocation || - typeof data.publishLocation.name !== 'string' || - typeof data.publishLocation.identifier !== 'string' || - typeof data.publishLocation.service !== 'string' - ) { - console.warn('Invalid PUBLISH_STATUS data, skipping:', data); - return; +export interface MultiplePublishError { + error: { + unsuccessfulPublishes: any[] } +} - const { publishLocation, chunks, totalChunks } = data; - const key = `${publishLocation?.service}-${publishLocation?.name}-${publishLocation?.identifier}`; +export const MultiPublishDialogComponent = () => { + const { + resources, + isPublishing, + failedResources, + reset, + reject, + error: publishError, + isLoading, + setIsLoading, + setError, + setFailedPublishResources, + complete + } = useMultiplePublishStore((state) => ({ + resources: state.resources, + isPublishing: state.isPublishing, + failedResources: state.failedResources, + reset: state.reset, + reject: state.reject, + error: state.error, + isLoading: state.isLoading, + setIsLoading: state.setIsLoading, + setError: state.setError, + setFailedPublishResources: state.setFailedPublishResources, + complete: state.complete + })); - try { - const dataToBeSent: any = {}; - if (chunks !== undefined && chunks !== null) { - dataToBeSent.chunks = chunks; - } - if (totalChunks !== undefined && totalChunks !== null) { - dataToBeSent.totalChunks = totalChunks; - } - - setPublishStatusByKey(key, { - publishLocation, - ...dataToBeSent, - processed: data?.processed || false -}); - } catch (err) { - console.error('Failed to set publish status:', err); - } - } + const { publishStatus, setPublishStatusByKey } = usePublishStatusStore(); + + const resourcesToPublish = useMemo(() => { + return resources.filter((item) => + failedResources.some((f) => + f?.name === item?.name && f?.identifier === item?.identifier && f?.service === item?.service + ) + ); + }, [resources, failedResources]); + + const publishMultipleResources = useCallback(async () => { + const timeout = resources.length * 1200000; + + try { + resourcesToPublish.forEach((item) => { + const key = `${item?.service}-${item?.name}-${item?.identifier}`; + setPublishStatusByKey(key, { + error: undefined, + chunks: 0, + totalChunks: 0 + }); + }); + + setIsLoading(true); + setError(null); + setFailedPublishResources([]); + + const result = await qortalRequestWithTimeout( + { action: 'PUBLISH_MULTIPLE_QDN_RESOURCES', resources: resourcesToPublish }, + timeout + ); + + complete(result); + } catch (error: any) { + const unPublished = error?.error?.unsuccessfulPublishes; + const failedPublishes: QortalGetMetadata[] = []; + + if (unPublished && Array.isArray(unPublished)) { + unPublished.forEach((item) => { + const key = `${item?.service}-${item?.name}-${item?.identifier}`; + setPublishStatusByKey(key, { + error: { reason: item?.reason } + }); + + failedPublishes.push({ + name: item?.name, + service: item?.service, + identifier: item?.identifier + }); + }); + setFailedPublishResources(failedPublishes); + } else { + setError(error instanceof Error ? error.message : 'Error during publish'); + } + } finally { + setIsLoading(false); + } + }, [resourcesToPublish, resources, setPublishStatusByKey, setIsLoading, setError, setFailedPublishResources, complete]); + + const handleNavigation = useCallback((event: any) => { + if (event.data?.action !== 'PUBLISH_STATUS') return; + + const data = event.data; + + if ( + !data.publishLocation || + typeof data.publishLocation?.name !== 'string' || + typeof data.publishLocation?.service !== 'string' + ) { + console.warn('Invalid PUBLISH_STATUS data, skipping:', data); + return; } - window.addEventListener("message", handleNavigation); + const { + publishLocation, + chunks, + totalChunks, + retry, + filename, + processed + } = data; - return () => { - window.removeEventListener("message", handleNavigation); + const key = `${publishLocation?.service}-${publishLocation?.name}-${publishLocation?.identifier}`; + + const update: any = { + publishLocation, + processed: processed || false }; - }, []); - if(!isPublishing) return null + if (chunks != null && chunks !== undefined) update.chunks = chunks; + if (totalChunks != null && totalChunks !== undefined) update.totalChunks = totalChunks; + if (retry != null && retry !== undefined) update.retry = retry; + if (filename != null && retry !== undefined) update.filename = filename; + + try { + setPublishStatusByKey(key, update); + } catch (err) { + console.error('Failed to set publish status:', err); + } + }, [setPublishStatusByKey]); + + useEffect(() => { + window.addEventListener("message", handleNavigation); + return () => window.removeEventListener("message", handleNavigation); + }, [handleNavigation]); + + if (!isPublishing) return null; + return ( - + Publishing Status - - {resources.map((publish: ResourceToPublish) => { - const key = `${publish?.service}-${publish?.name}-${publish?.identifier}`; - const individualPublishStatus = publishStatus[key] || null - return ( - - ); - })} - + {publishError && ( + + + + {publishError} + + + )} + + {!publishError && ( + + {resources.map((publish) => { + const key = `${publish?.service}-${publish?.name}-${publish?.identifier}`; + const individualPublishStatus = publishStatus[key] || null; + return ( + + ); + })} + + )} + + + {failedResources?.length > 0 && ( + + )} + - ); }; - interface IndividualResourceComponentProps { publish: ResourceToPublish publishStatus: PublishStatus publishKey: string } -const ESTIMATED_PROCESSING_MS = 5 * 60 * 1000; // 5 minutes -const IndividualResourceComponent = ({publish, publishKey, publishStatus}: IndividualResourceComponentProps)=> { - const [now, setNow] = useState(Date.now()); -console.log('key500',publishKey, publishStatus) +const IndividualResourceComponent = ({ publish, publishKey, publishStatus }: IndividualResourceComponentProps) => { + const [now, setNow] = useState(Date.now()); + const [processingStart, setProcessingStart] = useState(); - const chunkPercent = useMemo(()=> { - if(!publishStatus?.chunks || !publishStatus?.totalChunks) return 0 - return (publishStatus?.chunks / publishStatus?.totalChunks) * 100 - }, [publishStatus]) - console.log('chunkPercent', chunkPercent) - const chunkDone = useMemo(()=> { - if(!publishStatus?.chunks || !publishStatus?.totalChunks) return false - return publishStatus?.chunks === publishStatus?.totalChunks - }, [publishStatus]) + const chunkPercent = useMemo(() => { + if (!publishStatus?.chunks || !publishStatus?.totalChunks) return 0; + return (publishStatus.chunks / publishStatus.totalChunks) * 100; + }, [publishStatus?.chunks, publishStatus?.totalChunks]); - useEffect(() => { - if(!chunkDone) return - const interval = setInterval(() => { - setNow(Date.now()); - }, 1000); + const chunkDone = useMemo(() => { + return ( + publishStatus?.chunks > 0 && + publishStatus?.totalChunks > 0 && + publishStatus?.chunks === publishStatus?.totalChunks + ); + }, [publishStatus?.chunks, publishStatus?.totalChunks]); + + // Start processing timer once chunking completes + useEffect(() => { + if (chunkDone && !processingStart) { + setProcessingStart(Date.now()); + } + }, [chunkDone, processingStart]); + + // Keep time ticking for progress simulation + useEffect(() => { + if (!chunkDone) return; + const interval = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(interval); }, [chunkDone]); - const [processingStart, setProcessingStart] = useState(); - -useEffect(() => { - if (chunkDone && !processingStart) { - setProcessingStart(Date.now()); - } -}, [chunkDone, processingStart]); - const processingPercent = useMemo(() => { - if (!chunkDone || !processingStart || !publishStatus?.totalChunks || !now) return 0; + if (publishStatus?.error || !chunkDone || !processingStart || !publishStatus?.totalChunks || !now) return 0; - const totalMB = publishStatus.totalChunks * 5; - const estimatedProcessingMs = (300_000 / 2048) * totalMB; + const totalMB = publishStatus.totalChunks * 5; // assume 5MB per chunk + const estimatedProcessingMs = (300_000 / 2048) * totalMB; // 5min per 2GB scaled - const elapsed = now - processingStart; - if(!elapsed || elapsed < 0) return 0 - return Math.min((elapsed / estimatedProcessingMs) * 100, 100); -}, [chunkDone, processingStart, now, publishStatus?.totalChunks]); + const elapsed = now - processingStart; + if (elapsed <= 0) return 0; + return Math.min((elapsed / estimatedProcessingMs) * 100, 100); + }, [chunkDone, processingStart, now, publishStatus?.totalChunks, publishStatus?.error]); return ( - - - {publishKey} - + + + {publish?.filename || publishStatus?.filename || publishKey} + - - - File Chunk { (!publishStatus?.chunks || !publishStatus?.totalChunks) ? '' : publishStatus?.chunks}/{publishStatus?.totalChunks} ({(chunkPercent).toFixed(0)}%) - - - + + + File Chunk {publishStatus?.chunks || 0}/{publishStatus?.totalChunks || 0} ({chunkPercent.toFixed(0)}%) + + + + + + + File Processing ({processingStart ? processingPercent.toFixed(0) : '0'}%) + + + + + {publishStatus?.processed && ( + + + Published successfully + + )} + + {publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && ( + + + Publish failed. Attempting retry... + + )} + + {publishStatus?.error && !publishStatus?.processed && ( + + + + Publish failed. {publishStatus?.error?.reason || 'Unknown error'} + + + )} + + ); +}; - - - File Processing ({(!processingPercent || !processingStart) ? '0' : processingPercent.toFixed(0)}%) - - - - - ) -} export const MultiPublishDialog = React.memo(MultiPublishDialogComponent); diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index 4bfaf7e..0f973ac 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -119,17 +119,14 @@ const SubtitleManagerComponent = ({ const subtitles = useListReturn( `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}` ); - console.log("subtitles222", subtitles); const mySubtitles = useMemo(() => { if (!auth?.name) return []; return subtitles?.filter((sub) => sub.name === auth?.name); }, [subtitles, auth?.name]); - console.log("subtitles222", subtitles); const getPublishedSubtitles = useCallback(async () => { try { setIsLoading(true); const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; - console.log("videoId", videoId); const postIdSearch = await identifierOperations.buildLooseSearchPrefix( ENTITY_SUBTITLE, videoId @@ -149,7 +146,6 @@ const SubtitleManagerComponent = ({ searchParams ); lists.addList(`subs-${videoId}`, res?.filter((item)=> !!item?.metadata?.title) || []); - console.log("resres2", res); } catch (error) { console.error(error); } finally { @@ -174,7 +170,6 @@ const SubtitleManagerComponent = ({ ]); const ref = useRef(null) - console.log('isOpen', open) useEffect(()=> { if(open){ @@ -184,7 +179,6 @@ const SubtitleManagerComponent = ({ const handleBlur = (e: React.FocusEvent) => { if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish) { - console.log('handleBlur') close(); setIsOpenPublish(false) } @@ -235,7 +229,6 @@ const SubtitleManagerComponent = ({ data: data, }); } - console.log("resources", resources); await qortalRequest({ action: "PUBLISH_MULTIPLE_QDN_RESOURCES", @@ -253,7 +246,6 @@ const SubtitleManagerComponent = ({ }; const onSelectHandler = (sub: SubtitlePublishedData) => { - console.log("onSelectHandler"); onSelect(sub); close(); }; @@ -608,7 +600,6 @@ const PublishSubtitles = ({ return copyPrev; }); }; - console.log("subtitles", subtitles); const handleClose = () => { setIsOpen(false); @@ -843,7 +834,6 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { const [selectedToDownload, setSelectedToDownload] = useState(null) const [isReady, setIsReady] = useState(false) const { resource, isLoading, error, refetch } = usePublish(2, "JSON", sub, true); - console.log("resource", resource, isLoading); const isSelected = currentSubtrack === resource?.data?.language; const [isGettingStatus, setIsGettingStatus] = useState(true) // useEffect(()=> { @@ -852,7 +842,6 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { // onSelect(resource?.data) // } // }, [isSelected, resource?.data]) - console.log('isReady', resource?.data) const getStatus = useCallback(async (service: Service, name: string, identifier: string)=> { try { if(subtitlesStatus[`${service}-${name}-${identifier}`]){ @@ -890,7 +879,6 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { getStatus(sub?.service, sub?.name, sub?.identifier) } }, [sub?.identifier, sub?.name, sub?.service]) -console.log('tester', isReady,isLoading,error,resource?.data) return ( { const x = e.clientX - rect.left; const percent = x / rect.width; const time = Math.min(Math.max(0, percent * duration), duration); - console.log("hello100"); setHoverX(e.clientX); setShowDuration(time); @@ -129,7 +128,6 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => { console.log("thumbnailUrl", thumbnailUrl, hoverX); } - console.log("duration", duration); return ( { const theme = useTheme() const ref = useRef(null) - console.log('isOpen', isOpen) useEffect(()=> { if(isOpen){ diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 1615cad..c1ab055 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -47,7 +47,6 @@ export async function srtBase64ToVttBlobUrl( // Step 2: Create a Blob from the Uint8Array with correct MIME type const srtBlob = new Blob([bytes], { type: "application/x-subrip" }); - console.log("srtBlob", srtBlob); // Step 3: Use convert() with the Blob const vttBlobUrl: string = await convert(srtBlob); return vttBlobUrl; @@ -93,14 +92,7 @@ const videoStyles = { video: {}, }; -async function loadMediaInfo(wasmPath = "/MediaInfoModule.wasm") { - const mediaInfoModule = await import("mediainfo.js"); - return await mediaInfoModule.default({ - format: "JSON", - full: true, - locateFile: () => wasmPath, - }); -} + async function getVideoMimeTypeFromUrl( qortalVideoResource: any @@ -114,71 +106,6 @@ async function getVideoMimeTypeFromUrl( } catch (error) { return null; } - // const mediaInfo = await loadMediaInfo(); - // const chunkCache = new Map(); - - // let fileSize = 0; - // try { - // const headResp = await fetch(videoUrl, { method: 'HEAD' }); - // const lengthHeader = headResp.headers.get('Content-Length'); - // if (!lengthHeader) throw new Error('Missing content length'); - // fileSize = parseInt(lengthHeader, 10); - // } catch (err) { - // console.error('Error fetching content length:', err); - // return null; - // } - - // try { - // const rawResult = await mediaInfo.analyzeData( - // () => fileSize, - // async (chunkSize: number, offset: number): Promise => { - // const key = `${offset}:${chunkSize}`; - // if (chunkCache.has(key)) return chunkCache.get(key)!; - - // const end = Math.min(fileSize - 1, offset + chunkSize - 1); - // const resp = await fetch(videoUrl, { - // headers: { Range: `bytes=${offset}-${end}` }, - // }); - - // if (!resp.ok || (resp.status !== 206 && fileSize > chunkSize)) { - // console.warn(`Range request failed: ${resp.status}`); - // return new Uint8Array(); - // } - - // const blob = await resp.blob(); - // const buffer = new Uint8Array(await blob.arrayBuffer()); - // chunkCache.set(key, buffer); - // return buffer; - // } - // ); - - // const result = JSON.parse(rawResult); - // const tracks = result?.media?.track; - - // const videoTrack = tracks?.find((t: any) => t['@type'] === 'Video'); - // const format = videoTrack?.Format?.toLowerCase(); - - // switch (format) { - // case 'avc': - // case 'h264': - // case 'mpeg-4': - // case 'mp4': - // return 'video/mp4'; - // case 'vp8': - // case 'vp9': - // return 'video/webm'; - // case 'hevc': - // case 'h265': - // return 'video/mp4'; // still usually wrapped in MP4 - // case 'matroska': - // return 'video/webm'; - // default: - // return 'video/mp4'; // fallback - // } - // } catch (err) { - // console.error('Error analyzing media info:', err); - // return null; - // } } export const VideoPlayer = ({ @@ -259,7 +186,6 @@ export const VideoPlayer = ({ } },[location]) - console.log('isFullscreen', isFullscreen) const { getProgress } = useProgressStore(); const enterFullscreen = useCallback(() => { @@ -276,7 +202,6 @@ export const VideoPlayer = ({ }, [isFullscreen]); const toggleFullscreen = useCallback(() => { - console.log('togglefull', isFullscreen) isFullscreen ? exitFullscreen() : enterFullscreen(); }, [isFullscreen]); @@ -531,7 +456,6 @@ const videoLocationRef = useRef< null | string>(null) return } - console.log("onSelectSubtitle", subtitle); const player = playerRef.current; if (!player || !subtitle.subtitleData || !subtitle.type) return; @@ -604,16 +528,13 @@ const videoLocationRef = useRef< null | string>(null) }, 1000); }); const tracksInfo = playerRef.current?.textTracks(); - console.log("tracksInfo", tracksInfo); if (!tracksInfo) return; const tracks = Array.from( { length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i] ); - console.log("tracks", tracks); for (const track of tracks) { - console.log("track", track); if (track.kind === "subtitles") { track.mode = "showing"; // force display @@ -706,7 +627,6 @@ savedVideoRef.current = video.current; { length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i] ); - console.log("tracks", tracks); for (const track of tracks) { if (track.kind === 'subtitles' || track.kind === 'captions') { @@ -718,10 +638,7 @@ savedVideoRef.current = video.current; } if (activeTrack) { - console.log("Subtitle active:", { - label: activeTrack.label, - srclang: activeTrack.language || activeTrack.srclang, // srclang for native, language for VTT - }); + setCurrentSubTrack(activeTrack.language || activeTrack.srclang) } else { setCurrentSubTrack(null) @@ -748,12 +665,10 @@ savedVideoRef.current = video.current; console.error("useEffect start player", error); } return () => { - console.log('hello1002') const video = savedVideoRef as any const videoEl = video?.current!; const player = playerRef.current; - console.log('videohello', videoEl); const isPlaying = !player?.paused(); if (videoEl && isPlaying && videoLocationRef.current) { diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index 6ae896f..fe3335d 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -140,7 +140,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => { const toggleMute = useCallback(() => { try { - console.log('toggleMute') const ref = playerRef as any; if (!ref.current) return; diff --git a/src/hooks/useGlobalPipPlayer.tsx b/src/hooks/useGlobalPipPlayer.tsx index d15089e..7b86190 100644 --- a/src/hooks/useGlobalPipPlayer.tsx +++ b/src/hooks/useGlobalPipPlayer.tsx @@ -16,8 +16,8 @@ export const GlobalPipPlayer = () => { const [playing , setPlaying] = useState(false) const [hasStarted, setHasStarted] = useState(false) const playerRef = useRef(null); - const {navigate} = useContext(GlobalContext) - + const context = useContext(GlobalContext) + const navigate = context?.navigate const videoNode = useRef(null); const { setProgress } = useProgressStore(); @@ -26,7 +26,6 @@ export const GlobalPipPlayer = () => { if (!player || typeof player?.currentTime !== "function") return; const currentTime = player.currentTime(); - console.log('videoId', videoId) if (typeof currentTime === "number" && videoId && currentTime > 0.1) { setProgress(videoId, currentTime); } diff --git a/src/hooks/usePublish.tsx b/src/hooks/usePublish.tsx index 8d89098..882584d 100644 --- a/src/hooks/usePublish.tsx +++ b/src/hooks/usePublish.tsx @@ -5,8 +5,9 @@ import { base64ToObject, retryTransaction } from "../utils/publish"; import { useGlobal } from "../context/GlobalProvider"; import { ReturnType } from "../components/ResourceList/ResourceListDisplay"; import { useCacheStore } from "../state/cache"; -import { useMultiplePublishStore } from "../state/multiplePublish"; +import { useMultiplePublishStore, usePublishStatusStore } from "../state/multiplePublish"; import { ResourceToPublish } from "../types/qortalRequests/types"; +import { MultiplePublishError } from "../components/MultiPublish/MultiPublishDialog"; interface StoredPublish { qortalMetadata: QortalMetadata; @@ -31,7 +32,7 @@ interface StoredPublish { }>; updatePublish: (publish: QortalGetMetadata, data: any) => Promise; deletePublish: (publish: QortalGetMetadata) => Promise; - publishMultipleResources: (resources: ResourceToPublish[])=> void + publishMultipleResources: (resources: ResourceToPublish[])=> Promise }; type UsePublishWithoutMetadata = { @@ -42,7 +43,7 @@ interface StoredPublish { }>; updatePublish: (publish: QortalGetMetadata, data: any) => Promise; deletePublish: (publish: QortalGetMetadata) => Promise; - publishMultipleResources: (resources: ResourceToPublish[])=> void + publishMultipleResources: (resources: ResourceToPublish[])=> Promise }; @@ -77,7 +78,7 @@ interface StoredPublish { const getPublish = usePublishStore(state=> state.getPublish) const setResourceCache = useCacheStore((s) => s.setResourceCache); const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted); - + const setPublishStatusByKey = usePublishStatusStore((s)=> s.setPublishStatusByKey) const setPublishResources = useMultiplePublishStore((state) => state.setPublishResources); const resetPublishResources = useMultiplePublishStore((state) => state.reset); const [hasResource, setHasResource] = useState(null); @@ -271,19 +272,55 @@ interface StoredPublish { }, [getStorageKey, setPublish]); - const publishMultipleResources = useCallback(async (resources: ResourceToPublish[]) => { - try { - setPublishResources(resources) - const lengthOfResources = resources?.length; - const lengthOfTimeout = lengthOfResources * 1200000; // Time out in QR, Seconds = 20 Minutes - return await qortalRequestWithTimeout({ +const publishMultipleResources = useCallback(async (resources: ResourceToPublish[]): Promise => { + return new Promise(async (resolve, reject) => { + const store = useMultiplePublishStore.getState(); + store.setPublishResources(resources); + store.setIsPublishing(true); + store.setCompletionResolver(resolve); + store.setRejectionResolver(reject); + try { + store.setIsLoading(true); + setPublishResources(resources); + store.setError(null) + store.setFailedPublishResources([]) + const lengthOfResources = resources?.length; + const lengthOfTimeout = lengthOfResources * 1200000; // 20 minutes per resource + + const result = await qortalRequestWithTimeout({ action: "PUBLISH_MULTIPLE_QDN_RESOURCES", resources }, lengthOfTimeout); - } catch (error) { - - } - }, [getStorageKey, setPublish]); + store.complete(result); + } catch (error: any) { + const unPublished = error?.error?.unsuccessfulPublishes; + const failedPublishes: QortalGetMetadata[] = [] + if (unPublished && Array.isArray(unPublished)) { + unPublished.forEach((item) => { + const key = `${item?.service}-${item?.name}-${item?.identifier}`; + + setPublishStatusByKey(key, { + error: { + reason: item.reason + } + }); + failedPublishes.push({ + name: item?.name, + service: item?.service, + identifier: item?.identifier + }) + }); + store.setFailedPublishResources(failedPublishes) + } else { + store.setError(error?.message || 'Error during publish') + } + + } finally { + store.setIsLoading(false); + } +}) +}, [setPublishResources]); + if (!metadata) return { diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index 80495f2..692c8f9 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -207,10 +207,8 @@ export const useResources = (retryAttempts: number = 2, maxSize = 5242880) => { if (cancelRequests) { cancelAllRequests(); } - console.log('listName', listName) const cacheKey = generateCacheKey(params); const searchCache = getSearchCache(listName, cacheKey); - console.log('searchCache', searchCache) if (searchCache) { const copyParams = {...params} delete copyParams.after @@ -223,12 +221,10 @@ export const useResources = (retryAttempts: number = 2, maxSize = 5242880) => { let responseData: QortalMetadata[] = []; let filteredResults: QortalMetadata[] = []; let lastCreated = params.before || undefined; - console.log('lastCreated', lastCreated) const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20 const isUnlimited = params.limit === 0; while (isUnlimited || filteredResults.length < targetLimit) { - console.log('beforebefore') const response = await qortalRequest({ action: "SEARCH_QDN_RESOURCES", mode: "ALL", @@ -236,14 +232,12 @@ export const useResources = (retryAttempts: number = 2, maxSize = 5242880) => { limit: targetLimit - filteredResults.length, // Adjust limit dynamically before: lastCreated, }); - console.log('responseresponse', response) if (!response || response.length === 0) { break; // No more data available } responseData = response; const validResults = responseData.filter((item) => item.size !== 32 && item.size < maxSize); - console.log('validResults', validResults) filteredResults = [...filteredResults, ...validResults]; if (filteredResults.length >= targetLimit && !isUnlimited) { @@ -312,7 +306,6 @@ export const useResources = (retryAttempts: number = 2, maxSize = 5242880) => { const addNewResources = useCallback( (listName: string, resources: Resource[]) => { - console.log('resources1212', resources) addTemporaryResource( listName, resources.map((item) => item.qortalMetadata) diff --git a/src/state/multiplePublish.ts b/src/state/multiplePublish.ts index da8c048..25c627f 100644 --- a/src/state/multiplePublish.ts +++ b/src/state/multiplePublish.ts @@ -1,23 +1,75 @@ import { create } from 'zustand'; import { ResourceToPublish } from '../types/qortalRequests/types'; -import { Service } from '../types/interfaces/resources'; +import { QortalGetMetadata, Service } from '../types/interfaces/resources'; interface MultiplePublishState { resources: ResourceToPublish[]; - setPublishResources: (resources: ResourceToPublish[])=> void - reset: ()=> void - isPublishing: boolean -} - const initialState = { - resources: [], - isPublishing: false - }; -export const useMultiplePublishStore = create((set) => ({ - ...initialState, - setPublishResources: (resources: ResourceToPublish[]) => set(() => ({ resources, isPublishing: true })), - reset: () => set(initialState), + failedResources: QortalGetMetadata[]; + isPublishing: boolean; + resolveCallback?: (result: QortalGetMetadata[]) => void; + rejectCallback?: (error: Error) => void; + setPublishResources: (resources: ResourceToPublish[]) => void; + setFailedPublishResources: (resources: QortalGetMetadata[]) => void; + setIsPublishing: (value: boolean) => void; + setCompletionResolver: (resolver: (result: QortalGetMetadata[]) => void) => void; + setRejectionResolver: (resolver: (reject: Error) => void) => void; + complete: (result: any) => void; + reject: (Error: Error) => void; + reset: () => void; + setError: (message: string | null)=> void + error: string | null + isLoading: boolean + setIsLoading: (val: boolean)=> void +} + +const initialState = { + resources: [], + failedResources: [], + isPublishing: false, + resolveCallback: undefined, + rejectCallback: undefined, + error: "", + isLoading: false +}; + +export const useMultiplePublishStore = create((set, get) => ({ + ...initialState, + + setPublishResources: (resources) => { + set({ resources, isPublishing: true }); + }, + setFailedPublishResources: (resources) => { + set({ failedResources: resources }); + }, + setIsPublishing: (value) => { + set({ isPublishing: value }); + }, + setIsLoading: (value) => { + set({ isLoading: value }); + }, + setCompletionResolver: (resolver) => { + set({ resolveCallback: resolver }); + }, +setRejectionResolver: (reject) => { + set({ rejectCallback: reject }); + }, + complete: (result) => { + const resolver = get().resolveCallback; + if (resolver) resolver(result); + set({ resolveCallback: undefined, isPublishing: false }); + }, + reject: (result) => { + const resolver = get().rejectCallback; + if (resolver) resolver(result); + set({ resolveCallback: undefined, isPublishing: false }); + }, + setError: (message) => { + set({ error: message }); + }, + + reset: () => set(initialState), })); export type PublishLocation = { @@ -31,6 +83,11 @@ export type PublishStatus = { chunks: number; totalChunks: number; processed: boolean; + error?: { + reason: string + } + retry: boolean + filename: string }; type PublishStatusStore = { @@ -58,6 +115,7 @@ export const usePublishStatusStore = create((set, get) => ({ chunks: 0, totalChunks: 0, processed: false, + retry: false }; const newStatus: PublishStatus = { diff --git a/src/utils/base64.ts b/src/utils/base64.ts index a8e63b7..16a7be3 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -121,7 +121,6 @@ export function base64ToObject(base64: string){ } export const base64ToBlobUrl = (base64: string, mimeType = 'text/vtt'): string => { - console.log('base64ToBlobUrl', base64, mimeType) const cleanedBase64 = base64.length % 4 === 0 ? base64 : base64 + '='.repeat(4 - base64.length % 4); try { diff --git a/tsup.config.ts b/tsup.config.ts index ca9a14f..6ff7fdc 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -9,7 +9,6 @@ export default defineConfig({ '@mui/material', '@mui/system', '@emotion/react', - '@emotion/styled', - 'mediainfo.js' + '@emotion/styled' ], });