Initial commit: Video player with HLS support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-02 01:52:03 -06:00
commit 1743d472d2
29 changed files with 14074 additions and 0 deletions

54
server/api/config.post.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineEventHandler, readBody, createError } from 'h3'
import { promises as fs } from 'fs'
import { join } from 'path'
const CONFIG_FILE = join(process.cwd(), '.autostart-config')
const CORRECT_PASSWORD = '431522'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
// Verificar contraseña
if (body.password !== CORRECT_PASSWORD) {
throw createError({
statusCode: 401,
message: 'Contraseña incorrecta'
})
}
// Actualizar configuración
if (body.action === 'toggle-autostart') {
const currentConfig = await fs.readFile(CONFIG_FILE, 'utf-8').catch(() => 'enabled')
const newConfig = currentConfig.trim() === 'enabled' ? 'disabled' : 'enabled'
await fs.writeFile(CONFIG_FILE, newConfig, 'utf-8')
return {
success: true,
autostart: newConfig === 'enabled'
}
}
// Obtener estado actual
if (body.action === 'get-status') {
const currentConfig = await fs.readFile(CONFIG_FILE, 'utf-8').catch(() => 'enabled')
return {
success: true,
autostart: currentConfig.trim() === 'enabled'
}
}
throw createError({
statusCode: 400,
message: 'Acción inválida'
})
} catch (error: any) {
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: 'Error al actualizar configuración'
})
}
})

140
server/api/videos.get.ts Normal file
View File

@@ -0,0 +1,140 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { stat } from 'fs/promises'
export default defineEventHandler(async (event) => {
try {
const videosPath = join(process.cwd(), 'videos')
// Verificar si existe la carpeta
try {
await fs.access(videosPath)
} catch {
return { videos: [] }
}
const files = await fs.readdir(videosPath)
// Detectar videos originales (archivos de video sin sufijos de calidad)
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
const videoFiles = files.filter(file => {
const ext = file.toLowerCase().substring(file.lastIndexOf('.'))
const nameWithoutExt = file.substring(0, file.lastIndexOf('.'))
const hasQualitySuffix = /_(4k|2k|1080p|720p|480p|360p)$/i.test(nameWithoutExt)
return videoExtensions.includes(ext) && !hasQualitySuffix
})
const videoMap = new Map<string, any>()
// Procesar cada video original
for (const file of videoFiles) {
const ext = file.substring(file.lastIndexOf('.'))
const baseName = file.substring(0, file.lastIndexOf('.'))
// Verificar si existe carpeta HLS
const hlsDir = join(videosPath, `${baseName}_hls`)
let isHLS = false
let hlsQualities: any[] = []
try {
const hlsStat = await stat(hlsDir)
if (hlsStat.isDirectory()) {
// Buscar archivos .m3u8 para detectar calidades disponibles
const hlsFiles = await fs.readdir(hlsDir)
const qualityFiles = hlsFiles.filter(f =>
f.endsWith('.m3u8') &&
f !== 'master.m3u8' &&
!f.includes('_vtt') // Excluir archivos de subtítulos
)
qualityFiles.forEach(qFile => {
const quality = qFile.replace('.m3u8', '')
hlsQualities.push({
quality,
label: quality.toUpperCase(),
url: `/videos/${baseName}_hls/${qFile}`,
file: qFile,
isHLS: true
})
})
if (hlsQualities.length > 0) {
isHLS = true
}
}
} catch {
// No existe HLS para este video
}
if (isHLS) {
// Video con HLS
videoMap.set(baseName, {
id: baseName,
name: baseName,
extension: ext.toLowerCase(),
isHLS: true,
qualities: hlsQualities,
url: `/videos/${baseName}_hls/master.m3u8`
})
} else {
// Video sin HLS, buscar versiones de calidad tradicionales
const qualities = [
{
quality: 'auto',
label: 'Original',
url: `/videos/${file}`,
file,
isHLS: false
}
]
// Buscar versiones con sufijos de calidad
for (const qualitySuffix of ['4k', '2k', '1080p', '720p', '480p', '360p']) {
const qualityFile = `${baseName}_${qualitySuffix}${ext}`
if (files.includes(qualityFile)) {
qualities.push({
quality: qualitySuffix,
label: qualitySuffix.toUpperCase(),
url: `/videos/${qualityFile}`,
file: qualityFile,
isHLS: false
})
}
}
videoMap.set(baseName, {
id: baseName,
name: baseName,
extension: ext.toLowerCase(),
isHLS: false,
qualities,
url: `/videos/${file}`
})
}
}
// Convertir a array y ordenar calidades
const videos = Array.from(videoMap.values()).map(video => {
// Ordenar calidades de mayor a menor
const qualityOrder = { '4k': 0, '2k': 1, '1080p': 2, '720p': 3, '480p': 4, '360p': 5, 'auto': 6 }
video.qualities.sort((a: any, b: any) => {
return (qualityOrder[a.quality as keyof typeof qualityOrder] || 999) -
(qualityOrder[b.quality as keyof typeof qualityOrder] || 999)
})
// Establecer la URL por defecto (la mejor calidad disponible)
if (video.isHLS) {
video.url = `/videos/${video.name}_hls/master.m3u8`
} else {
video.url = video.qualities[0].url
}
return video
})
return { videos }
} catch (error) {
console.error('Error reading videos:', error)
return { videos: [], error: 'Failed to load videos' }
}
})