first commit
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
<?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->getName();
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
return $this->render('/frontend/test.html.twig', []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user