
Teljes kódot kijavítva vissza küldöd. minden egyes karaktert...
Prompt
Teljes kódot kijavítva vissza küldöd. minden egyes karaktert leírsz, nem rövidítesz. minden hibát kijavítasz. egy fájl marad. ne írj semmi mást csak a teljes kódot es kommentek nem lehetnek benne! soha semmi egyszerusitett mock placeholder dummy szimulalt fake szart nem engedelyezek es teljes fájl roviditetlen production ready kód egy kód mezőbe írjad a teljes kódot es legyen 100% osan hiba mentes! import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { buildDownloadPlan, createSink, loadMasterPlaylist, runDownload, sanitizeFileName, supportsFileSystemAccess, } from '@/lib/download'; import type { DownloadPlan, DownloadProgress, MasterPlaylist, Variant } from '@/lib/download'; export interface DownloaderState { open: boolean; master: MasterPlaylist | null; plan: DownloadPlan | null; variantIndex: number; loadingManifest: boolean; loadingPlan: boolean; running: boolean; error: string; notice: string; progress: DownloadProgress; } const IDLE_PROGRESS: DownloadProgress = { phase: 'idle', message: '', segmentsDone: 0, segmentsTotal: 0, bytesWritten: 0, bytesDownloaded: 0, bytesPerSecond: 0, etaSeconds: 0, percent: 0, }; function describeError(error: unknown): string { if (error instanceof DOMException && error.name === 'AbortError') return 'A mentés megszakítva.'; if (error instanceof TypeError) { return 'A tartalomkiszolgáló nem engedélyezi a közvetlen letöltést (CORS), vagy a hálózat megszakadt.'; } if (error instanceof Error) return error.message; return 'Ismeretlen hiba a mentés közben.'; } export function useDownloader(sourceUrl: string, titleHint: string) { const [state, setState] = useState<DownloaderState>({ open: false, master: null, plan: null, variantIndex: 0, loadingManifest: false, loadingPlan: false, running: false, error: '', notice: '', progress: IDLE_PROGRESS, }); const abortRef = useRef<AbortController | null>(null); const manifestAbortRef = useRef<AbortController | null>(null); useEffect(() => { setState((current) => ({ ...current, master: null, plan: null, variantIndex: 0, error: '', notice: '', progress: IDLE_PROGRESS, })); }, [sourceUrl]); useEffect( () => () => { abortRef.current?.abort(); manifestAbortRef.current?.abort(); }, [] ); const selectedVariant: Variant | null = useMemo(() => { if (!state.master) return null; return state.master.variants[state.variantIndex] ?? state.master.variants[0] ?? null; }, [state.master, state.variantIndex]); const audioUri = useMemo(() => { if (!state.master || !selectedVariant) return null; if (!selectedVariant.audioGroup) return null; const group = state.master.audio.filter((item) => item.groupId === selectedVariant.audioGroup && item.uri); if (group.length === 0) return null; const preferred = group.find((item) => item.isDefault) ?? group[0]; return preferred.uri; }, [state.master, selectedVariant]); const loadPlan = useCallback( async (variant: Variant, audio: string | null) => { manifestAbortRef.current?.abort(); const controller = new AbortController(); manifestAbortRef.current = controller; setState((current) => ({ ...current, loadingPlan: true, error: '', plan: null })); try { const plan = await buildDownloadPlan(variant, audio, controller.signal); if (controller.signal.aborted) return; const drm = plan.video.drmProtected || (plan.audio?.drmProtected ?? false); setState((current) => ({ ...current, plan, loadingPlan: false, error: drm ? 'A kiválasztott sáv DRM-mel titkosított, a fájl mentése nem végezhető el.' : '', notice: plan.video.isLive ? 'Élő adás: a mentés a jelenleg elérhető ablakot rögzíti.' : '', })); } catch (error) { if (controller.signal.aborted) return; setState((current) => ({ ...current, loadingPlan: false, error: describeError(error) })); } }, [] ); const openPanel = useCallback(async () => { setState((current) => ({ ...current, open: true })); if (state.master) return; manifestAbortRef.current?.abort(); const controller = new AbortController(); manifestAbortRef.current = controller; setState((current) => ({ ...current, loadingManifest: true, error: '' })); try { const master = await loadMasterPlaylist(sourceUrl, controller.signal); if (controller.signal.aborted) return; setState((current) => ({ ...current, master, loadingManifest: false, variantIndex: 0 })); const variant = master.variants[0]; if (!variant) { setState((current) => ({ ...current, error: 'A manifestben nincs menthető sáv.' })); return; } const group = variant.audioGroup ? master.audio.filter((item) => item.groupId === variant.audioGroup && item.uri) : []; const audio = group.length > 0 ? (group.find((item) => item.isDefault) ?? group[0]).uri : null; await loadPlan(variant, audio); } catch (error) { if (controller.signal.aborted) return; setState((current) => ({ ...current, loadingManifest: false, error: describeError(error) })); } }, [sourceUrl, state.master, loadPlan]); const closePanel = useCallback(() => { setState((current) => ({ ...current, open: false })); }, []); const selectVariant = useCallback( (index: number) => { setState((current) => ({ ...current, variantIndex: index })); const master = state.master; if (!master) return; const variant = master.variants[index]; if (!variant) return; const group = variant.audioGroup ? master.audio.filter((item) => item.groupId === variant.audioGroup && item.uri) : []; const audio = group.length > 0 ? (group.find((item) => item.isDefault) ?? group[0]).uri : null; void loadPlan(variant, audio); }, [state.master, loadPlan] ); const cancel = useCallback(() => { abortRef.current?.abort(); }, []); const start = useCallback(async () => { const plan = state.plan; const variant = selectedVariant; if (!plan || !variant || state.running) return; if (plan.video.drmProtected || (plan.audio?.drmProtected ?? false)) return; const base = sanitizeFileName(`${titleHint || 'stream'}-${variant.height > 0 ? `${variant.height}p` : 'audio'}`); const fileName = `${base}${plan.extension}`; let sink; try { // A fájlválasztónak azonnal, a kattintás aktivációja alatt kell megnyílnia. sink = await createSink(fileName, plan.mimeType, plan.extension); } catch (error) { setState((current) => ({ ...current, error: describeError(error) })); return; } const controller = new AbortController(); abortRef.current = controller; setState((current) => ({ ...current, running: true, error: '', notice: sink.kind === 'memory' ? 'A böngésző nem támogatja a közvetlen lemezre írást, a fájl a memóriában készül el.' : current.notice, progress: { ...IDLE_PROGRESS, phase: 'downloading', message: 'Indítás…', segmentsTotal: plan.segmentCount }, })); try { await runDownload({ plan, sink, fileBaseName: base, signal: controller.signal, onProgress: (progress) => setState((current) => ({ ...current, progress })), }); setState((current) => ({ ...current, running: false })); } catch (error) { await sink.abort(); const aborted = controller.signal.aborted; setState((current) => ({ ...current, running: false, error: aborted ? '' : describeError(error), progress: { ...current.progress, phase: aborted ? 'cancelled' : 'error', message: aborted ? 'Megszakítva.' : describeError(error), }, })); } finally { abortRef.current = null; } }, [state.plan, state.running, selectedVariant, titleHint]); return { ...state, selectedVariant, audioUri, directDiskWrite: supportsFileSystemAccess(), openPanel, closePanel, selectVariant, start, cancel, }; }