Files
downloader_video/src/Controller/Frontend/HomeController.php
T
2026-06-06 16:07:01 +02:00

356 lines
12 KiB
PHP

<?php
namespace App\Controller\Frontend;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints\Url;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use App\Entity\UserDownloader;
use App\Entity\AnonymeDownloader;
use App\Service\Frontend\DownloadService;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\Process\Exception\ProcessFailedException;
final class HomeController extends AbstractController
{
private string $projectDir;
public function __construct(
private EntityManagerInterface $em,
private readonly Filesystem $filesystem,
private readonly TranslatorInterface $translator,
private ParameterBagInterface $params,
private DownloadService $downloadService
) {
$this->projectDir = $params->get('kernel.project_dir');
}
#[Route('/', name: 'app_frontend_home')]
public function index(): Response
{
return $this->render('/frontend/index.html.twig', []);
}
#[Route('/size-movie', name: 'app_frontend_size_movie')]
public function sizeMovie(Request $request): Response
{
$url = $request->get('url');
// 2. Validation stricte du format URL
$validator = Validation::createValidator();
$violations = $validator->validate($url, [
new Url(['protocols' => ['http', 'https']])
]);
if (count($violations) > 0 || empty($url)) {
return new Response("URL invalide ou manquante.", Response::HTTP_BAD_REQUEST);
}
$process = new Process([
'yt-dlp',
'--print',
'%(title)s|%(filesize,filesize_approx)s',
'--', // 🛡️ Protection cruciale contre l'injection d'arguments
$url
]);
$process->run();
if (!$process->isSuccessful()) {
//echo $process->getCommandLine() . "<br>" . $process->getErrorOutput();
//die;
return new Response("error1", Response::HTTP_INTERNAL_SERVER_ERROR);
}
// 2. On récupère la sortie et on nettoie les espaces
$output = trim($process->getOutput());
// 3. On sépare le titre et la taille grâce au délimiteur '|'
$parts = explode('|', $output);
// On vérifie qu'on a bien reçu les deux éléments attendus
if (count($parts) >= 2) {
$filename = $parts[0];
$bytes = trim($parts[1]);
$megabytes = "Inconnu";
if (is_numeric($bytes)) {
// Calcul de conversion en Mo
$megabytes = round($bytes / 1024 / 1024, 2);
}
// Option A : Retourner une réponse JSON (Recommandé si vous utilisez du JS côté front)
return new JsonResponse([
'filename' => $filename,
'size' => $megabytes
]);
}
return new Response("error2", Response::HTTP_BAD_REQUEST);
}
#[Route('/download-streamed', name: 'app_streamed_download')]
public function downloadStreamed(Request $request): StreamedResponse
{
$url = $request->query->get('url');
$pathDownload = $this->projectDir . '/public/downloader/anonyme/';
// Résoudre l'userId AVANT la closure (évite $this->getUser() dans le stream)
$user = $this->getUser();
if ($user) {
$userId = $user?->getId();
$pathDownload = $this->projectDir . '/public/downloader/user/' . $userId . '/';
if (!is_dir($pathDownload)) {
mkdir($pathDownload, 0777, true);
}
}
// Récupérer le nom du fichier AVANT le stream
$filenameProcess = new Process(['yt-dlp', '--get-filename', '-o', '%(id)s.%(ext)s|%(title)s', $url]);
$filenameProcess->run();
$output = trim($filenameProcess->getOutput());
// 3. On sépare le titre et la taille grâce au délimiteur '|'
$parts = explode('|', $output);
if (count($parts) >= 2) {
$fileName = $parts[0];
$name = $parts[1];
}
set_time_limit(0);
// Capturer le service AVANT la closure
$downloadService = $this->downloadService;
$response = new StreamedResponse(function () use ($url, $pathDownload, $fileName, $name, $user, $downloadService) {
// ✅ CRUCIAL : détecter la déconnexion client
ignore_user_abort(false);
$process = new Process(['yt-dlp', '-o', $pathDownload . $fileName, '--newline', $url]);
$process->setTimeout(3600);
// ✅ Stocker le process pour pouvoir le tuer
register_shutdown_function(function () use ($process) {
if ($process->isRunning()) {
$process->stop(3); // SIGTERM, puis SIGKILL après 3s
}
});
$process->run(function ($type, $buffer) use ($fileName, $process) {
// ✅ Vérifier la déconnexion dans le callback de progression
if (connection_aborted()) {
$process->stop(3);
exit();
}
if (Process::ERR !== $type) {
if (preg_match('/(\d+(?:\.\d+)?)\%/', $buffer, $matches)) {
$percentage = round((float)$matches[1]);
echo "data: " . json_encode([
'progress' => $percentage,
'filename' => $fileName,
]) . "\n\n";
if (ob_get_level() > 0) ob_flush();
flush();
}
}
});
// ✅ Vérifier que le process s'est bien terminé (pas tué/annulé)
if (!$process->isSuccessful()) {
echo "data: " . json_encode([
'error' => true,
'message' => 'Le téléchargement a échoué ou a été interrompu',
]) . "\n\n";
if (ob_get_level() > 0) ob_flush();
flush();
return; // ← stop, pas de signal "done"
}
// Signal de fin
echo "data: " . json_encode([
'done' => true,
'pathDownload' => $downloadService->downloadSaveStream($url, $fileName, $name, $pathDownload, $user),
]) . "\n\n";
if (ob_get_level() > 0) ob_flush();
flush();
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('Connection', 'keep-alive');
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
#[Route('/download-background', name: 'app_background_download')]
public function download(MessageBusInterface $bus, Request $request): Response
{
$url = $request->get('url');
$user = $this->getUser();
$userDownloader = new UserDownloader();
$userDownloader->setUrl($url);
$userDownloader->setUser($user);
$userDownloader->setDateAdd(new \DateTime());
$userDownloader->setIsDelete(false);
// 1. Récupérer d'abord le titre/nom du fichier que yt-dlp va générer
// On utilise --get-filename pour simuler le nom final sans télécharger tout de suite
$getNameProcess = new Process(['yt-dlp', '--get-filename', '-o', '%(id)s.%(ext)s|%(title)s', $url]);
$getNameProcess->run();
if (!$getNameProcess->isSuccessful()) {
throw new ProcessFailedException($getNameProcess);
}
$output = trim($getNameProcess->getOutput());
$parts = explode('|', $output);
if (count($parts) >= 2) {
$filename = $parts[0];
$name = $parts[1];
$userDownloader->setName($name);
$userDownloader->setNameFile($filename);
// On envoie le message dans la file d'attente
$bus->dispatch($userDownloader);
return new Response('Le téléchargement a démarré en tâche de fond !');
}else{
return new Response($output, Response::HTTP_BAD_REQUEST);
}
}
#[Route('/history', name: 'app_frontend_history')]
#[IsGranted('ROLE_USER')]
public function history(): Response
{
return $this->render('/frontend/history.html.twig', [
'videos' => $this->em->getRepository(UserDownloader::class)->findBy(['user' => $this->getUser(), 'isDelete' => false])
]);
}
#[Route('/download/user/{id}', name: 'app_frontend_download_user')]
#[IsGranted('ROLE_USER')]
public function downloadUser(UserDownloader $userDownloader): BinaryFileResponse
{
$id = $userDownloader->getId();
$fileName = $userDownloader->getNameFile();
$user = $userDownloader->getUser();
$filePath = $this->projectDir . '/public/downloader/user/' . $user->getId() . '/' . $fileName;
// Fichier introuvable sur le disque
if (!$this->filesystem->exists($filePath)) {
throw $this->createNotFoundException(
$this->translator->trans('error_file_not_found', [], 'home')
);
}
return $this->downloadService->downloadUser($id, $fileName, $filePath);
}
#[Route('/download/anonyme/{id}', name: 'app_frontend_download_anonyme')]
public function downloadAnonyme(AnonymeDownloader $anonymeDownloader): BinaryFileResponse
{
$id = $anonymeDownloader->getId();
$fileName = $anonymeDownloader->getNameFile();
$filePath = $this->projectDir . '/public/downloader/anonyme/' . $fileName;
// Fichier introuvable sur le disque
if (!$this->filesystem->exists($filePath)) {
throw $this->createNotFoundException(
$this->translator->trans('error_file_not_found', [], 'home')
);
}
return $this->downloadService->downloadUser($id, $fileName, $filePath);
}
#[Route('/delete-video/{id}', name: 'app_frontend_delete_video', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function deleteVideo(UserDownloader $userDownloader, Request $request): Response
{
// 1. Validation du token CSRF
if (!$this->isCsrfTokenValid('delete_video', $request->request->get('_token'))) {
$this->addFlash('danger', $this->translator->trans('error_invalid_csrf', [], 'home'));
return $this->redirectToRoute('app_frontend_history');
}
// 2. Suppression du fichier physique (si présent)
$path = $this->projectDir . '/public/downloader/user/';
$filePath = $path . $this->getUser()->getId() . '/' . $userDownloader->getNameFile();
if ($this->filesystem->exists($filePath)) {
$this->filesystem->remove($filePath);
}
// 3. Suppression en base
$userDownloader->setIsDelete(true);
$this->em->persist($userDownloader);
$this->em->flush();
// 4. Message flash de confirmation
$this->addFlash(
'success',
$this->translator->trans('flash_video_deleted', ['%name%' => $userDownloader->getName()], 'home')
);
return $this->redirectToRoute('app_frontend_history');
}
#[Route('/test', name: 'app_test')]
public function test()
{
die;
return $this->render('/frontend/test.html.twig', []);
}
}