diff --git a/assets/controllers/download_list_controller.js b/assets/controllers/download_list_controller.js index 8e051fc..a3ea22d 100644 --- a/assets/controllers/download_list_controller.js +++ b/assets/controllers/download_list_controller.js @@ -30,7 +30,13 @@ export default class extends Controller { } pauseDownload(data) { - fetch(`/api/download/${data.params.id}`, {method: 'PUT'}) + fetch(`/api/download/${data.params.id}/pause`, {method: 'PATCH'}) + .then(res => res.json()) + .then(json => console.debug(json)); + } + + resumeDownload(data) { + fetch(`/api/download/${data.params.id}/resume`, {method: 'PATCH'}) .then(res => res.json()) .then(json => console.debug(json)); } diff --git a/assets/icons/icon-park-twotone/play.svg b/assets/icons/icon-park-twotone/play.svg new file mode 100644 index 0000000..c9cd051 --- /dev/null +++ b/assets/icons/icon-park-twotone/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Download/Action/Command/ResumeDownloadCommand.php b/src/Download/Action/Command/ResumeDownloadCommand.php new file mode 100644 index 0000000..fb62b21 --- /dev/null +++ b/src/Download/Action/Command/ResumeDownloadCommand.php @@ -0,0 +1,15 @@ + + */ +class ResumeDownloadCommand implements CommandInterface +{ + public function __construct( + public int $downloadId, + ) {} +} \ No newline at end of file diff --git a/src/Download/Action/Handler/DownloadMediaHandler.php b/src/Download/Action/Handler/DownloadMediaHandler.php index 4adcff9..d8d2726 100644 --- a/src/Download/Action/Handler/DownloadMediaHandler.php +++ b/src/Download/Action/Handler/DownloadMediaHandler.php @@ -38,7 +38,9 @@ readonly class DownloadMediaHandler implements HandlerInterface } try { - $this->downloadRepository->updateStatus($download->getId(), 'In Progress'); + if ($download->getStatus() !== 'Paused') { + $this->downloadRepository->updateStatus($download->getId(), 'In Progress'); + } $this->downloader->download( $command->mediaType, diff --git a/src/Download/Action/Handler/PauseDownloadHandler.php b/src/Download/Action/Handler/PauseDownloadHandler.php index 986b070..75b622a 100644 --- a/src/Download/Action/Handler/PauseDownloadHandler.php +++ b/src/Download/Action/Handler/PauseDownloadHandler.php @@ -6,7 +6,6 @@ use App\Download\Action\Command\PauseDownloadCommand; use App\Download\Action\Result\PauseDownloadResult; use App\Download\Framework\Entity\Download; use App\Download\Framework\Repository\DownloadRepository; -use App\Monitor\Service\MediaFiles; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; use OneToMany\RichBundle\Contract\ResultInterface; @@ -15,11 +14,8 @@ use Symfony\Contracts\Cache\CacheInterface; /** @implements HandlerInterface */ readonly class PauseDownloadHandler implements HandlerInterface { - const PAUSED_EXTENSION = '.paused'; - public function __construct( private DownloadRepository $downloadRepository, - private MediaFiles $mediaFiles, private CacheInterface $cache, ) {} @@ -32,8 +28,6 @@ readonly class PauseDownloadHandler implements HandlerInterface return true; }); - $download->setFilename($download->getFilename() . self::PAUSED_EXTENSION); - return new PauseDownloadResult(200, 'Success', $download); } } diff --git a/src/Download/Action/Handler/ResumeDownloadHandler.php b/src/Download/Action/Handler/ResumeDownloadHandler.php new file mode 100644 index 0000000..02fad03 --- /dev/null +++ b/src/Download/Action/Handler/ResumeDownloadHandler.php @@ -0,0 +1,40 @@ + */ +readonly class ResumeDownloadHandler implements HandlerInterface +{ + public function __construct( + private DownloadRepository $downloadRepository, + private MessageBusInterface $messageBus, + ) {} + + public function handle(CommandInterface $command): ResultInterface + { + /** @var Download $download */ + $download = $this->downloadRepository->find($command->downloadId); + + $this->messageBus->dispatch(new DownloadMediaCommand( + $download->getUrl(), + $download->getTitle(), + $download->getFilename(), + $download->getMediaType(), + $download->getImdbId(), + $download->getUser()->getId(), + $download->getId() + )); + + return new ResumeDownloadResult(200, 'Success', $download); + } +} diff --git a/src/Download/Action/Input/ResumeDownloadInput.php b/src/Download/Action/Input/ResumeDownloadInput.php new file mode 100644 index 0000000..74b7a68 --- /dev/null +++ b/src/Download/Action/Input/ResumeDownloadInput.php @@ -0,0 +1,24 @@ + */ +class ResumeDownloadInput implements InputInterface +{ + public function __construct( + #[SourceRoute('downloadId')] + public int $downloadId, + ) {} + + public function toCommand(): CommandInterface + { + return new PauseDownloadCommand( + $this->downloadId, + ); + } +} \ No newline at end of file diff --git a/src/Download/Action/Result/ResumeDownloadResult.php b/src/Download/Action/Result/ResumeDownloadResult.php new file mode 100644 index 0000000..7179f71 --- /dev/null +++ b/src/Download/Action/Result/ResumeDownloadResult.php @@ -0,0 +1,16 @@ + */ +class ResumeDownloadResult implements ResultInterface +{ + public function __construct( + public int $status, + public string $message, + public Download $download, + ) {} +} diff --git a/src/Download/Downloader/ProcessDownloader.php b/src/Download/Downloader/ProcessDownloader.php index 260d765..4ef843e 100644 --- a/src/Download/Downloader/ProcessDownloader.php +++ b/src/Download/Downloader/ProcessDownloader.php @@ -5,12 +5,17 @@ namespace App\Download\Downloader; use App\Download\Framework\Entity\Download; use App\Monitor\Service\MediaFiles; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use Symfony\Contracts\Cache\CacheInterface; class ProcessDownloader implements DownloaderInterface { + /** + * @var RedisAdapter $cache + */ + public function __construct( private EntityManagerInterface $entityManager, private MediaFiles $mediaFiles, @@ -24,16 +29,23 @@ class ProcessDownloader implements DownloaderInterface { /** @var Download $downloadEntity */ $downloadEntity = $this->entityManager->getRepository(Download::class)->find($downloadId); - $downloadEntity->setProgress(0); $this->entityManager->flush(); $downloadPreferences = $downloadEntity->getUser()->getDownloadPreferences(); $path = $this->getDownloadPath($mediaType, $title, $downloadPreferences); - $process = (new Process([ - 'wget', - $url, - ]))->setWorkingDirectory($path); + $processArgs = ['wget', $url]; + + if ($downloadEntity->getStatus() === 'Paused') { + $downloadEntity->setStatus('In Progress'); + $processArgs = ['wget', '-c', $url]; + } else { + $downloadEntity->setProgress(0); + } + + fwrite(STDOUT, implode(" ", $processArgs)); + + $process = (new Process($processArgs))->setWorkingDirectory($path); $process->setTimeout(1800); // 30 min $process->setIdleTimeout(600); // 10 min @@ -48,10 +60,10 @@ class ProcessDownloader implements DownloaderInterface // The PauseDownloadHandler will set this to 'true' $doPause = $this->cache->getItem('download.pause.' . $downloadEntity->getId()); - if (true === $doPause->isHit()) { + if (true === $doPause->isHit() && true === $doPause->get()) { $downloadEntity->setStatus('Paused'); + $this->cache->deleteItem('download.pause.' . $downloadEntity->getId()); $this->entityManager->flush(); - $doPause->expiresAt(new \DateTimeImmutable('now')); $process->stop(); } diff --git a/src/Download/Framework/Controller/ApiController.php b/src/Download/Framework/Controller/ApiController.php index 9e54742..5b94c5b 100644 --- a/src/Download/Framework/Controller/ApiController.php +++ b/src/Download/Framework/Controller/ApiController.php @@ -4,9 +4,11 @@ namespace App\Download\Framework\Controller; use App\Download\Action\Handler\DeleteDownloadHandler; use App\Download\Action\Handler\PauseDownloadHandler; +use App\Download\Action\Handler\ResumeDownloadHandler; use App\Download\Action\Input\DeleteDownloadInput; use App\Download\Action\Input\DownloadMediaInput; use App\Download\Action\Input\PauseDownloadInput; +use App\Download\Action\Input\ResumeDownloadInput; use App\Download\Framework\Repository\DownloadRepository; use App\Util\Broadcaster; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -68,7 +70,7 @@ class ApiController extends AbstractController return $this->json(['status' => 200, 'message' => 'Download Deleted']); } - #[Route('/api/download/{downloadId}', name: 'api_download_pause', methods: ['PUT'])] + #[Route('/api/download/{downloadId}/pause', name: 'api_download_pause', methods: ['PATCH'])] public function pauseDownload( PauseDownloadInput $input, PauseDownloadHandler $handler, @@ -81,4 +83,18 @@ class ApiController extends AbstractController return $this->json(['status' => 200, 'message' => 'Download Paused']); } + + #[Route('/api/download/{downloadId}/resume', name: 'api_download_resume', methods: ['PATCH'])] + public function resumeDownload( + ResumeDownloadInput $input, + ResumeDownloadHandler $handler, + ): Response { + $result = $handler->handle($input->toCommand()); + $this->broadcaster->alert( + title: 'Success', + message: "{$result->download->getTitle()} has been Resumed.", + ); + + return $this->json(['status' => 200, 'message' => 'Download Resumed']); + } } diff --git a/templates/broadcast/Download.stream.html.twig b/templates/broadcast/Download.stream.html.twig index 53e5afa..3915d19 100644 --- a/templates/broadcast/Download.stream.html.twig +++ b/templates/broadcast/Download.stream.html.twig @@ -14,10 +14,31 @@ {% if entity.status != "Complete" %} + + + {% else %} @@ -27,6 +48,9 @@ + + +