356 lines
12 KiB
PHP
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', []);
|
|
}
|
|
}
|