import { useCallback, useEffect, useRef, useState } from 'react';
import { api } from './api';
import { isFinished } from './format';
import { useDownloadSocket } from './useDownloadSocket';
function blank(id, patch) {
return {
id,
url: '',
platform: 'yt',
status: 'pending',
progress: 0,
speed: null,
eta: null,
size: null,
title: null,
uploader: null,
thumbnail: null,
outputPath: null,
errorMessage: null,
...patch,
};
}
export function useSessions() {
const [sessions, setSessions] = useState(new Map());
const sessionsRef = useRef(sessions);
sessionsRef.current = sessions;
const dismissedRef = useRef(new Set());
const patchSession = useCallback((id, patch) => {
if (dismissedRef.current.has(id)) return;
setSessions((prev) => {
const next = new Map(prev);
const existing = next.get(id) || blank(id);
next.set(id, { ...existing, ...patch });
return next;
});
}, []);
const handleEvent = useCallback(
(message) => {
switch (message.type) {
case 'status':
patchSession(message.session_id, { status: message.status });
break;
case 'metadata':
patchSession(message.session_id, {
title: message.title,
uploader: message.uploader,
thumbnail: message.thumbnail,
duration: message.duration,
});
break;
case 'progress':
patchSession(message.session_id, {
progress: message.percent,
speed: message.speed,
eta: message.eta,
size: message.size,
});
break;
case 'complete':
patchSession(message.session_id, {
status: 'complete',
progress: 100,
outputPath: message.output_path,
});
break;
case 'error':
patchSession(message.session_id, {
status: message.message === 'cancelled by user' ? 'cancelled' : 'error',
errorMessage: message.message,
});
break;
default:
break;
}
},
[patchSession],
);
const { connection, subscribe } = useDownloadSocket(handleEvent);
const addLaunched = useCallback(
(id, patch) => {
setSessions((prev) => {
if (prev.has(id)) return prev;
const next = new Map(prev);
next.set(id, blank(id, patch));
return next;
});
subscribe(id);
},
[subscribe],
);
const refresh = useCallback(async () => {
let list;
try {
list = await api('GET', '/api/downloads');
} catch {
return;
}
setSessions((prev) => {
const next = new Map(prev);
list.forEach((item) => {
if (dismissedRef.current.has(item.session_id)) return;
const existing = next.get(item.session_id);
next.set(item.session_id, {
...(existing || blank(item.session_id)),
url: item.url,
platform: item.platform,
status: item.status,
progress: item.progress,
errorMessage: item.error_message ?? (existing ? existing.errorMessage : null),
});
if (!isFinished(item.status)) subscribe(item.session_id);
});
return next;
});
}, [subscribe]);
useEffect(() => {
refresh();
const interval = setInterval(refresh, 8000);
return () => clearInterval(interval);
}, [refresh]);
const cancel = useCallback(async (id) => {
await api('DELETE', `/api/download/${id}`);
}, []);
const dismiss = useCallback((id) => {
dismissedRef.current.add(id);
setSessions((prev) => {
if (!prev.has(id)) return prev;
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);
const list = Array.from(sessions.values()).sort((a, b) => {
const aFinished = isFinished(a.status);
const bFinished = isFinished(b.status);
if (aFinished !== bFinished) return aFinished ? 1 : -1;
return 0;
});
return { sessions: list, connection, addLaunched, cancel, dismiss };
}