stream local files to player

This commit is contained in:
2026-01-05 13:21:54 +01:00
parent 5b5cedcc98
commit 5fd2341e8e
15 changed files with 1869 additions and 275 deletions

View File

@@ -2,6 +2,7 @@ import {FastifyReply, FastifyRequest} from 'fastify';
import fs from 'fs';
import * as service from './local.service';
import * as downloadService from './download.service';
import * as streamingService from './streaming.service';
type ScanQuery = {
mode?: 'full' | 'incremental';
@@ -21,7 +22,7 @@ type DownloadAnimeBody =
| {
anilist_id: number;
episode_number: number;
stream_url: string; // media playlist FINAL
stream_url: string;
is_master?: false;
subtitles?: {
language: string;
@@ -36,28 +37,24 @@ type DownloadAnimeBody =
| {
anilist_id: number;
episode_number: number;
stream_url: string; // master.m3u8
stream_url: string;
is_master: true;
variant: {
resolution: string;
bandwidth?: number;
codecs?: string;
playlist_url: string;
};
audio?: {
group?: string;
language?: string;
name?: string;
playlist_url: string;
}[];
subtitles?: {
language: string;
url: string;
}[];
chapters?: {
title: string;
start_time: number;
@@ -266,7 +263,6 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
const clientHeaders = (request.body as any).headers || {};
// Validación básica
if (!anilist_id || !episode_number || !stream_url) {
return reply.status(400).send({
error: 'MISSING_REQUIRED_FIELDS',
@@ -274,17 +270,14 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
});
}
// Proxy del stream URL principal
const proxyUrl = buildProxyUrl(stream_url, clientHeaders);
console.log('Stream URL:', proxyUrl);
// Proxy de subtítulos
const proxiedSubs = subtitles?.map(sub => ({
...sub,
url: buildProxyUrl(sub.url, clientHeaders)
}));
// Preparar parámetros base
const downloadParams: any = {
anilistId: anilist_id,
episodeNumber: episode_number,
@@ -293,7 +286,6 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
chapters
};
// Si es master playlist, agregar campos adicionales
if (is_master === true) {
const { variant, audio } = request.body as any;
@@ -305,14 +297,11 @@ export async function downloadAnime(request: FastifyRequest<{ Body: DownloadAnim
}
downloadParams.is_master = true;
// Proxy del variant playlist
downloadParams.variant = {
...variant,
playlist_url: buildProxyUrl(variant.playlist_url, clientHeaders)
};
// Proxy de audio tracks si existen
if (audio && audio.length > 0) {
downloadParams.audio = audio.map((a: any) => ({
...a,
@@ -408,4 +397,116 @@ export async function downloadBook(request: FastifyRequest<{ Body: DownloadBookB
return reply.status(500).send({ error: 'FAILED_TO_DOWNLOAD_BOOK' });
}
}
// NUEVO: Estado de descargas
export async function getDownloadStatus(request: FastifyRequest, reply: FastifyReply) {
try {
const downloads = downloadService.getActiveDownloads();
const streams = streamingService.getActiveStreamsStatus();
return {
downloads: {
total: downloads.length,
active: downloads.filter(d => d.status === 'downloading').length,
completed: downloads.filter(d => d.status === 'completed').length,
failed: downloads.filter(d => d.status === 'failed').length,
list: downloads
},
streams: {
total: streams.length,
active: streams.filter(s => !s.isComplete).length,
completed: streams.filter(s => s.isComplete).length,
list: streams
}
};
} catch (err) {
console.error('Error getting download status:', err);
return reply.status(500).send({ error: 'FAILED_TO_GET_DOWNLOAD_STATUS' });
}
}
// NUEVO: Streaming HLS para anime local
export async function getAnimeStreamManifest(request: FastifyRequest, reply: FastifyReply) {
try {
const { type, id, unit } = request.params as any;
if (type !== 'anime') {
return reply.status(400).send({ error: 'ONLY_ANIME_SUPPORTED' });
}
const fileInfo = await service.getFileForStreaming(id, unit);
if (!fileInfo) {
return reply.status(404).send({ error: 'FILE_NOT_FOUND' });
}
const manifest = await streamingService.getStreamingManifest(fileInfo.filePath);
if (!manifest) {
return reply.status(500).send({ error: 'FAILED_TO_GENERATE_MANIFEST' });
}
return manifest;
} catch (err: any) {
console.error('Error getting stream manifest:', err);
return reply.status(500).send({ error: 'FAILED_TO_GET_STREAM_MANIFEST' });
}
}
// NUEVO: Servir archivos HLS
export async function serveHLSFile(request: FastifyRequest, reply: FastifyReply) {
try {
const { hash, filename } = request.params as any;
const file = await streamingService.getHLSFile(hash, filename);
if (!file) {
return reply.status(404).send({ error: 'FILE_NOT_FOUND' });
}
const contentType = filename.endsWith('.m3u8')
? 'application/vnd.apple.mpegurl'
: filename.endsWith('.vtt')
? 'text/vtt'
: 'video/mp2t';
reply
.header('Content-Type', contentType)
.header('Content-Length', file.stat.size)
.header('Access-Control-Allow-Origin', '*');
return fs.createReadStream(file.path);
} catch (err) {
console.error('Error serving HLS file:', err);
return reply.status(500).send({ error: 'FAILED_TO_SERVE_HLS_FILE' });
}
}
export async function getSubtitle(request: FastifyRequest, reply: FastifyReply) {
try {
const { id, unit, track } = request.params as any;
// Validar que el track sea un número
const trackIndex = parseInt(track, 10);
if (isNaN(trackIndex)) {
return reply.status(400).send({ error: 'INVALID_TRACK_INDEX' });
}
const subtitleStream = await service.extractSubtitleTrack(id, unit, trackIndex);
if (!subtitleStream) {
return reply.status(404).send({ error: 'FILE_NOT_FOUND' });
}
// Cabecera esencial para que el navegador entienda que son subtítulos
reply.header('Content-Type', 'text/vtt');
reply.header('Cache-Control', 'public, max-age=86400'); // Cachear por 1 día si quieres
return subtitleStream;
} catch (err) {
console.error('Error serving subtitles:', err);
return reply.status(500).send({ error: 'FAILED_TO_SERVE_SUBTITLES' });
}
}