added download monitor
This commit is contained in:
@@ -23,6 +23,8 @@ type DownloadStatus = {
|
||||
error?: string;
|
||||
startedAt: number;
|
||||
completedAt?: number;
|
||||
folderName?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
const activeDownloads = new Map<string, DownloadStatus>();
|
||||
@@ -50,6 +52,7 @@ type AnimeDownloadParams = {
|
||||
quality?: string;
|
||||
subtitles?: Array<{ language: string; url: string }>;
|
||||
chapters?: Array<{ title: string; start_time: number; end_time: number }>;
|
||||
totalDuration?: number;
|
||||
};
|
||||
|
||||
type BookDownloadParams = {
|
||||
@@ -137,10 +140,12 @@ async function getOrCreateEntry(
|
||||
}
|
||||
|
||||
export async function downloadAnimeEpisode(params: AnimeDownloadParams) {
|
||||
const { anilistId, episodeNumber, streamUrl, subtitles, chapters } = params;
|
||||
const { anilistId, episodeNumber, streamUrl, subtitles, chapters, totalDuration } = params;
|
||||
|
||||
const entry: any = await getOrCreateEntry(anilistId, 'anime');
|
||||
const fileName = `Episode_${episodeNumber.toString().padStart(2, '0')}.mkv`;
|
||||
|
||||
const downloadId = crypto.randomUUID();
|
||||
|
||||
activeDownloads.set(downloadId, {
|
||||
id: downloadId,
|
||||
type: 'anime',
|
||||
@@ -148,11 +153,11 @@ export async function downloadAnimeEpisode(params: AnimeDownloadParams) {
|
||||
unitNumber: episodeNumber,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
startedAt: Date.now()
|
||||
startedAt: Date.now(),
|
||||
folderName: entry.folderName,
|
||||
fileName: fileName
|
||||
});
|
||||
|
||||
const entry = await getOrCreateEntry(anilistId, 'anime');
|
||||
|
||||
const exists = await queryOne(
|
||||
`SELECT id FROM local_files WHERE entry_id = ? AND unit_number = ?`,
|
||||
[entry.id, episodeNumber],
|
||||
@@ -314,11 +319,21 @@ export async function downloadAnimeEpisode(params: AnimeDownloadParams) {
|
||||
const speedMatch = text.match(/speed=(\S+)/);
|
||||
|
||||
if (timeMatch || speedMatch) {
|
||||
updateDownloadProgress(downloadId, {
|
||||
timeElapsed: timeMatch?.[1],
|
||||
speed: speedMatch?.[1]
|
||||
});
|
||||
const updates: any = {};
|
||||
|
||||
if (timeMatch) updates.timeElapsed = timeMatch[1];
|
||||
if (speedMatch) updates.speed = speedMatch[1];
|
||||
|
||||
if (timeMatch && totalDuration && totalDuration > 0) {
|
||||
const elapsedSeconds = parseFFmpegTime(timeMatch[1]);
|
||||
updates.progress = Math.min(
|
||||
99,
|
||||
Math.round((elapsedSeconds / totalDuration) * 100)
|
||||
);
|
||||
}
|
||||
updateDownloadProgress(downloadId, updates);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ff.on('error', (error) => reject(error));
|
||||
@@ -381,6 +396,12 @@ export async function downloadAnimeEpisode(params: AnimeDownloadParams) {
|
||||
export async function downloadBookChapter(params: BookDownloadParams) {
|
||||
const { anilistId, chapterNumber, format, content, images } = params;
|
||||
|
||||
const type = format === 'manga' ? 'manga' : 'novels';
|
||||
const entry = await getOrCreateEntry(anilistId, type);
|
||||
|
||||
const ext = format === 'manga' ? 'cbz' : 'epub';
|
||||
const fileName = `Chapter_${chapterNumber.toString().padStart(3, '0')}.${ext}`;
|
||||
|
||||
const downloadId = crypto.randomUUID();
|
||||
|
||||
activeDownloads.set(downloadId, {
|
||||
@@ -390,12 +411,11 @@ export async function downloadBookChapter(params: BookDownloadParams) {
|
||||
unitNumber: chapterNumber,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
startedAt: Date.now()
|
||||
startedAt: Date.now(),
|
||||
folderName: entry.folderName,
|
||||
fileName: fileName
|
||||
});
|
||||
|
||||
const type = format === 'manga' ? 'manga' : 'novels';
|
||||
const entry = await getOrCreateEntry(anilistId, type);
|
||||
|
||||
const existingFile = await queryOne(
|
||||
`SELECT id FROM local_files WHERE entry_id = ? AND unit_number = ?`,
|
||||
[entry.id, chapterNumber],
|
||||
@@ -531,4 +551,15 @@ ${content}
|
||||
(err as any).details = error.message;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFFmpegTime(timeStr: string): number {
|
||||
const parts = timeStr.split(':');
|
||||
if (parts.length < 3) return 0;
|
||||
|
||||
const h = parseFloat(parts[0]) || 0;
|
||||
const m = parseFloat(parts[1]) || 0;
|
||||
const s = parseFloat(parts[2]) || 0;
|
||||
|
||||
return (h * 3600) + (m * 60) + s;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ type DownloadAnimeBody =
|
||||
language: string;
|
||||
url: string;
|
||||
}[];
|
||||
duration?: number;
|
||||
chapters?: {
|
||||
title: string;
|
||||
start_time: number;
|
||||
@@ -38,6 +39,7 @@ type DownloadAnimeBody =
|
||||
anilist_id: number;
|
||||
episode_number: number;
|
||||
stream_url: string;
|
||||
duration?: number;
|
||||
is_master: true;
|
||||
variant: {
|
||||
resolution: string;
|
||||
@@ -256,6 +258,7 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
|
||||
anilist_id,
|
||||
episode_number,
|
||||
stream_url,
|
||||
duration,
|
||||
is_master,
|
||||
subtitles,
|
||||
chapters
|
||||
@@ -283,7 +286,8 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
|
||||
episodeNumber: episode_number,
|
||||
streamUrl: proxyUrl,
|
||||
subtitles: proxiedSubs,
|
||||
chapters
|
||||
chapters,
|
||||
totalDuration: duration
|
||||
};
|
||||
|
||||
if (is_master === true) {
|
||||
|
||||
@@ -1779,11 +1779,19 @@ const AnimePlayer = (function() {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<div class="spinner" style="width:18px; height:18px; border-width:2px;"></div>`;
|
||||
|
||||
// --- CAMBIO AQUÍ: Calcular duración del video actual ---
|
||||
let totalDuration = 0;
|
||||
if (els.video && isFinite(els.video.duration) && els.video.duration > 0) {
|
||||
totalDuration = Math.floor(els.video.duration);
|
||||
}
|
||||
// -------------------------------------------------------
|
||||
|
||||
let body = {
|
||||
anilist_id: parseInt(_animeId),
|
||||
episode_number: parseInt(_currentEpisode),
|
||||
stream_url: _rawVideoData.url,
|
||||
headers: _rawVideoData.headers || {},
|
||||
duration: totalDuration, // <--- ENVIAMOS LA DURACIÓN
|
||||
chapters: _skipIntervals.map(i => ({
|
||||
title: i.type === 'op' ? 'Opening' : 'Ending',
|
||||
start_time: i.startTime,
|
||||
|
||||
@@ -388,6 +388,95 @@ const DashboardApp = {
|
||||
|
||||
Library: {
|
||||
tempMatchContext: null,
|
||||
pollInterval: null,
|
||||
updateDownloadStatus: async function() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/library/downloads/status`, {
|
||||
headers: window.AuthUtils.getSimpleAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
this.renderDownloadMonitor(data);
|
||||
|
||||
// Si hay descargas completadas nuevas, podríamos recargar la lista de archivos
|
||||
// (Opcional: lógica para detectar cambios y llamar a loadContent)
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error polling downloads:", e);
|
||||
}
|
||||
},
|
||||
|
||||
renderDownloadMonitor: function(data) {
|
||||
const monitor = document.getElementById('downloads-monitor');
|
||||
const listContainer = document.getElementById('downloads-list-container');
|
||||
const activeCountEl = document.getElementById('dl-stat-active');
|
||||
|
||||
// Datos por defecto
|
||||
const downloads = data.downloads || { list: [], active: 0, failed: 0 };
|
||||
|
||||
// Actualizar contadores cabecera
|
||||
if(activeCountEl) activeCountEl.textContent = `${downloads.active} Active / ${downloads.list.length} Total`;
|
||||
|
||||
// Ocultar si vacío
|
||||
if (downloads.list.length === 0) {
|
||||
monitor.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
monitor.classList.remove('hidden');
|
||||
|
||||
listContainer.innerHTML = downloads.list.map(item => {
|
||||
const fileName = item.fileName || `Unknown_File_${item.unitNumber}`;
|
||||
const folderName = item.folderName || 'Unsorted';
|
||||
const status = item.status || 'pending';
|
||||
const progress = item.progress || 0;
|
||||
const speed = item.speed || '0 KB/s';
|
||||
|
||||
const isCompleted = status === 'completed';
|
||||
const isFailed = status === 'failed';
|
||||
|
||||
let statusText = `${progress}%`;
|
||||
if (isCompleted) statusText = 'Done';
|
||||
if (isFailed) statusText = 'Failed';
|
||||
|
||||
const folderIcon = `<svg width="12" height="12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>`;
|
||||
|
||||
// ESTRUCTURA NUEVA: Más plana para permitir Flexbox horizontal
|
||||
return `
|
||||
<div class="dl-item compact">
|
||||
<div class="dl-left-col">
|
||||
<div class="dl-filename" title="${fileName}">${fileName}</div>
|
||||
<div class="dl-folder" title="${folderName}">${folderIcon} ${folderName}</div>
|
||||
</div>
|
||||
|
||||
<div class="dl-right-col">
|
||||
<div class="dl-meta-info">
|
||||
<span class="dl-speed">${isCompleted ? '' : speed}</span>
|
||||
<span class="dl-status-text ${status}">${statusText}</span>
|
||||
</div>
|
||||
<div class="dl-progress-track">
|
||||
<div class="dl-progress-fill ${status}" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
if (this.pollInterval) clearInterval(this.pollInterval);
|
||||
this.updateDownloadStatus(); // Primera llamada inmediata
|
||||
this.pollInterval = setInterval(() => this.updateDownloadStatus(), 2000); // Cada 2 segundos
|
||||
},
|
||||
|
||||
stopPolling: function() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
},
|
||||
loadStats: async function() {
|
||||
const types = ['anime', 'manga', 'novels'];
|
||||
const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' };
|
||||
@@ -584,6 +673,7 @@ const DashboardApp = {
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// Gestión de clases activas
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
@@ -593,9 +683,13 @@ const DashboardApp = {
|
||||
if (sec.id === targetId) sec.classList.add('active');
|
||||
});
|
||||
|
||||
// Lógica específica por pestaña
|
||||
if (tab.dataset.target === 'local') {
|
||||
DashboardApp.Library.loadStats();
|
||||
DashboardApp.Library.loadContent('anime');
|
||||
DashboardApp.Library.loadContent(DashboardApp.State.currentLocalType || 'anime');
|
||||
DashboardApp.Library.startPolling(); // <--- INICIAR POLLING
|
||||
} else {
|
||||
DashboardApp.Library.stopPolling(); // <--- DETENER POLLING AL SALIR
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user