feat: add submission form and status page
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Ombrora — Telechargeur de videos',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="fr">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { SubmitForm } from '@/components/SubmitForm'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main style={{ padding: '2rem' }}>
|
||||||
|
<h1>Ombrora</h1>
|
||||||
|
<SubmitForm />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { StatusView } from '@/components/StatusView'
|
||||||
|
|
||||||
|
export default async function StatusPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ uuid: string }>
|
||||||
|
}) {
|
||||||
|
const { uuid } = await params
|
||||||
|
return (
|
||||||
|
<main style={{ padding: '2rem' }}>
|
||||||
|
<h1>Statut du telechargement</h1>
|
||||||
|
<StatusView uuid={uuid} />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
type DownloadData = {
|
||||||
|
uuid: string
|
||||||
|
status: 'PENDING' | 'PROCESSING' | 'DONE' | 'FAILED' | 'FILE_DELETED'
|
||||||
|
format: string
|
||||||
|
quality: string
|
||||||
|
fileName: string | null
|
||||||
|
fileSize: string | null
|
||||||
|
errorMsg: string | null
|
||||||
|
downloadToken: string | null
|
||||||
|
tokenExpiresAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusView({ uuid }: { uuid: string }) {
|
||||||
|
const [data, setData] = useState<DownloadData | null>(null)
|
||||||
|
const [notFound, setNotFound] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
const res = await fetch(`/api/downloads/${uuid}`)
|
||||||
|
if (!active) return
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
setNotFound(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await res.json()) as DownloadData
|
||||||
|
setData(body)
|
||||||
|
|
||||||
|
if (body.status === 'PENDING' || body.status === 'PROCESSING') {
|
||||||
|
setTimeout(poll, 5_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll()
|
||||||
|
return () => { active = false }
|
||||||
|
}, [uuid])
|
||||||
|
|
||||||
|
if (notFound) return <p>Telechargement introuvable.</p>
|
||||||
|
if (!data) return <p>Chargement...</p>
|
||||||
|
|
||||||
|
if (data.status === 'PENDING' || data.status === 'PROCESSING') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Statut : {data.status === 'PENDING' ? 'En attente' : 'En cours...'}</p>
|
||||||
|
<p>Cette page se rafraichit automatiquement toutes les 5 secondes.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'DONE' && data.downloadToken) {
|
||||||
|
const sizeMb = data.fileSize
|
||||||
|
? `(${(Number(data.fileSize) / 1_048_576).toFixed(1)} Mo)`
|
||||||
|
: ''
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Telechargement pret {sizeMb}</p>
|
||||||
|
<a
|
||||||
|
href={`/api/download/${data.downloadToken}`}
|
||||||
|
download={data.fileName ?? undefined}
|
||||||
|
>
|
||||||
|
Telecharger {data.fileName}
|
||||||
|
</a>
|
||||||
|
{data.tokenExpiresAt && (
|
||||||
|
<p style={{ fontSize: '0.85rem', color: '#666' }}>
|
||||||
|
Lien valable jusqu'au{' '}
|
||||||
|
{new Date(data.tokenExpiresAt).toLocaleString('fr-FR')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'FAILED') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Echec du telechargement.</p>
|
||||||
|
<pre style={{ background: '#fee', padding: '0.5rem', overflowX: 'auto' }}>
|
||||||
|
{data.errorMsg}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'FILE_DELETED') {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Le fichier a expire et a ete supprime. Le telechargement n'est plus
|
||||||
|
disponible.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <p>Statut inconnu : {data.status}</p>
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, FormEvent } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
const FORMATS = ['mp4', 'mp3', 'webm', 'mkv']
|
||||||
|
const QUALITIES = ['best', '1080p', '720p', '480p', '360p']
|
||||||
|
|
||||||
|
export function SubmitForm() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [format, setFormat] = useState('mp4')
|
||||||
|
const [quality, setQuality] = useState('best')
|
||||||
|
const [subtitles, setSubtitles] = useState(false)
|
||||||
|
const [extraArgs, setExtraArgs] = useState('')
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const res = await fetch('/api/downloads', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url,
|
||||||
|
format,
|
||||||
|
quality,
|
||||||
|
subtitles,
|
||||||
|
extraArgs: extraArgs.trim() || null,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (res.status === 429) {
|
||||||
|
setError('Trop de soumissions. Reessayez dans une heure.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}))
|
||||||
|
setError((body as { error?: string }).error ?? 'Erreur lors de la soumission.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uuid } = (await res.json()) as { uuid: string }
|
||||||
|
router.push(`/status/${uuid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 480 }}>
|
||||||
|
<label>
|
||||||
|
URL de la video
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="https://www.youtube.com/watch?v=..."
|
||||||
|
style={{ display: 'block', width: '100%' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Format
|
||||||
|
<select value={format} onChange={(e) => setFormat(e.target.value)} style={{ display: 'block' }}>
|
||||||
|
{FORMATS.map((f) => <option key={f}>{f}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Qualite
|
||||||
|
<select value={quality} onChange={(e) => setQuality(e.target.value)} style={{ display: 'block' }}>
|
||||||
|
{QUALITIES.map((q) => <option key={q}>{q}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={subtitles}
|
||||||
|
onChange={(e) => setSubtitles(e.target.checked)}
|
||||||
|
/>{' '}
|
||||||
|
Telecharger les sous-titres (fr, en)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Options avancees (optionnel, JSON)
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={extraArgs}
|
||||||
|
onChange={(e) => setExtraArgs(e.target.value)}
|
||||||
|
placeholder='["--sponsorblock-remove","all"]'
|
||||||
|
style={{ display: 'block', width: '100%' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? 'Envoi...' : 'Telecharger'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user