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() . "
" . $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', []); } }