diff --git a/Dockerfile b/Dockerfile
index 9188c10..f5fe56e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM dunglas/frankenphp:php8.4-alpine
+FROM dunglas/frankenphp:php8.4
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
@@ -11,8 +11,8 @@ RUN install-php-extensions \
zip \
opcache
-RUN apk add --no-cache wget
+RUN apt update && apt install -y wget
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
-COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
+COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
diff --git a/assets/bootstrap.js b/assets/bootstrap.js
index 4d08a22..9b07d31 100644
--- a/assets/bootstrap.js
+++ b/assets/bootstrap.js
@@ -1,8 +1,10 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
import Popover from '@stimulus-components/popover'
import Dialog from '@stimulus-components/dialog'
+import Dropdown from '@stimulus-components/dropdown'
const app = startStimulusApp();
// register any custom, 3rd party controllers here
app.register('popover', Popover);
app.register('dialog', Dialog);
+app.register('dropdown', Dropdown);
diff --git a/assets/controllers/download_list_controller.js b/assets/controllers/download_list_controller.js
index 09889e0..8e051fc 100644
--- a/assets/controllers/download_list_controller.js
+++ b/assets/controllers/download_list_controller.js
@@ -29,6 +29,12 @@ export default class extends Controller {
}
}
+ pauseDownload(data) {
+ fetch(`/api/download/${data.params.id}`, {method: 'PUT'})
+ .then(res => res.json())
+ .then(json => console.debug(json));
+ }
+
deleteDownload(data) {
fetch(`/api/download/${data.params.id}`, {method: 'DELETE'})
.then(res => res.json())
diff --git a/assets/icons/icon-park-twotone/pause-one.svg b/assets/icons/icon-park-twotone/pause-one.svg
new file mode 100644
index 0000000..724fbf3
--- /dev/null
+++ b/assets/icons/icon-park-twotone/pause-one.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docker/app/entrypoint.sh b/docker/app/entrypoint.sh
index 31c36e4..cfe7bfe 100644
--- a/docker/app/entrypoint.sh
+++ b/docker/app/entrypoint.sh
@@ -9,4 +9,8 @@ sleep $SLEEP_TIME
php /app/bin/console doctrine:migrations:migrate --no-interaction
php /app/bin/console db:seed
-exec docker-php-entrypoint "$@"
+# Start the media cache warming services
+systemctl --user enable messenger-worker.service
+systemctl --user start messenger-worker.service
+
+frankenphp php-server /etc/frankenphp/Caddyfile
diff --git a/docker/app/messenger-worker@.service b/docker/app/messenger-worker@.service
new file mode 100644
index 0000000..dd4d4f4
--- /dev/null
+++ b/docker/app/messenger-worker@.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Torsearch media cache warming services
+
+[Service]
+ExecStart=php /app/bin/console messenger:consume media_cache --time-limit=3600
+# for Redis, set a custom consumer name for each instance
+Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i"
+Restart=always
+RestartSec=30
+
+[Install]
+WantedBy=default.target
diff --git a/importmap.php b/importmap.php
index a295579..3db9e68 100644
--- a/importmap.php
+++ b/importmap.php
@@ -34,4 +34,10 @@ return [
'@stimulus-components/dialog' => [
'version' => '1.0.1',
],
+ '@stimulus-components/dropdown' => [
+ 'version' => '3.0.0',
+ ],
+ 'stimulus-use' => [
+ 'version' => '0.52.2',
+ ],
];
diff --git a/src/Download/Action/Command/PauseDownloadCommand.php b/src/Download/Action/Command/PauseDownloadCommand.php
new file mode 100644
index 0000000..050939b
--- /dev/null
+++ b/src/Download/Action/Command/PauseDownloadCommand.php
@@ -0,0 +1,15 @@
+
+ */
+class PauseDownloadCommand 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 6207f51..4adcff9 100644
--- a/src/Download/Action/Handler/DownloadMediaHandler.php
+++ b/src/Download/Action/Handler/DownloadMediaHandler.php
@@ -47,7 +47,9 @@ readonly class DownloadMediaHandler implements HandlerInterface
$download->getId()
);
- $this->downloadRepository->updateStatus($download->getId(), 'Complete');
+ if ($download->getStatus() !== 'Paused') {
+ $this->downloadRepository->updateStatus($download->getId(), 'Complete');
+ }
} catch (\Throwable $exception) {
throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500);
diff --git a/src/Download/Action/Handler/PauseDownloadHandler.php b/src/Download/Action/Handler/PauseDownloadHandler.php
new file mode 100644
index 0000000..986b070
--- /dev/null
+++ b/src/Download/Action/Handler/PauseDownloadHandler.php
@@ -0,0 +1,39 @@
+ */
+readonly class PauseDownloadHandler implements HandlerInterface
+{
+ const PAUSED_EXTENSION = '.paused';
+
+ public function __construct(
+ private DownloadRepository $downloadRepository,
+ private MediaFiles $mediaFiles,
+ private CacheInterface $cache,
+ ) {}
+
+ public function handle(CommandInterface $command): ResultInterface
+ {
+ /** @var Download $download */
+ $download = $this->downloadRepository->find($command->downloadId);
+
+ $this->cache->get('download.pause.' . $download->getId(), function () {
+ return true;
+ });
+
+ $download->setFilename($download->getFilename() . self::PAUSED_EXTENSION);
+
+ return new PauseDownloadResult(200, 'Success', $download);
+ }
+}
diff --git a/src/Download/Action/Input/PauseDownloadInput.php b/src/Download/Action/Input/PauseDownloadInput.php
new file mode 100644
index 0000000..a6c5b5c
--- /dev/null
+++ b/src/Download/Action/Input/PauseDownloadInput.php
@@ -0,0 +1,24 @@
+ */
+class PauseDownloadInput 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/PauseDownloadResult.php b/src/Download/Action/Result/PauseDownloadResult.php
new file mode 100644
index 0000000..08066f2
--- /dev/null
+++ b/src/Download/Action/Result/PauseDownloadResult.php
@@ -0,0 +1,16 @@
+ */
+class PauseDownloadResult 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 b878a34..260d765 100644
--- a/src/Download/Downloader/ProcessDownloader.php
+++ b/src/Download/Downloader/ProcessDownloader.php
@@ -7,12 +7,14 @@ use App\Monitor\Service\MediaFiles;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
+use Symfony\Contracts\Cache\CacheInterface;
class ProcessDownloader implements DownloaderInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private MediaFiles $mediaFiles,
+ private CacheInterface $cache,
) {}
/**
@@ -30,7 +32,7 @@ class ProcessDownloader implements DownloaderInterface
$process = (new Process([
'wget',
- $url
+ $url,
]))->setWorkingDirectory($path);
$process->setTimeout(1800); // 30 min
@@ -41,7 +43,18 @@ class ProcessDownloader implements DownloaderInterface
try {
$progress = 0;
$this->entityManager->flush();
- $process->wait(function ($type, $buffer) use ($progress, $downloadEntity): void {
+
+ $process->wait(function ($type, $buffer) use ($progress, $downloadEntity, $process): void {
+ // The PauseDownloadHandler will set this to 'true'
+ $doPause = $this->cache->getItem('download.pause.' . $downloadEntity->getId());
+
+ if (true === $doPause->isHit()) {
+ $downloadEntity->setStatus('Paused');
+ $this->entityManager->flush();
+ $doPause->expiresAt(new \DateTimeImmutable('now'));
+ $process->stop();
+ }
+
if (Process::ERR === $type) {
$pregMatchOutput = [];
preg_match('/[\d]+%/', $buffer, $pregMatchOutput);
@@ -56,7 +69,9 @@ class ProcessDownloader implements DownloaderInterface
}
fwrite(STDOUT, $buffer);
});
- $downloadEntity->setProgress(100);
+ if ($downloadEntity->getStatus() !== 'Paused') {
+ $downloadEntity->setProgress(100);
+ }
} catch (ProcessFailedException $exception) {
$downloadEntity->setStatus('Failed');
}
diff --git a/src/Download/Framework/Controller/ApiController.php b/src/Download/Framework/Controller/ApiController.php
index b32edca..9e54742 100644
--- a/src/Download/Framework/Controller/ApiController.php
+++ b/src/Download/Framework/Controller/ApiController.php
@@ -3,8 +3,10 @@
namespace App\Download\Framework\Controller;
use App\Download\Action\Handler\DeleteDownloadHandler;
+use App\Download\Action\Handler\PauseDownloadHandler;
use App\Download\Action\Input\DeleteDownloadInput;
use App\Download\Action\Input\DownloadMediaInput;
+use App\Download\Action\Input\PauseDownloadInput;
use App\Download\Framework\Repository\DownloadRepository;
use App\Util\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -65,4 +67,18 @@ class ApiController extends AbstractController
return $this->json(['status' => 200, 'message' => 'Download Deleted']);
}
+
+ #[Route('/api/download/{downloadId}', name: 'api_download_pause', methods: ['PUT'])]
+ public function pauseDownload(
+ PauseDownloadInput $input,
+ PauseDownloadHandler $handler,
+ ): Response {
+ $result = $handler->handle($input->toCommand());
+ $this->broadcaster->alert(
+ title: 'Success',
+ message: "{$result->download->getTitle()} has been Paused.",
+ );
+
+ return $this->json(['status' => 200, 'message' => 'Download Paused']);
+ }
}
diff --git a/src/Download/Framework/Repository/DownloadRepository.php b/src/Download/Framework/Repository/DownloadRepository.php
index eb02681..de29746 100644
--- a/src/Download/Framework/Repository/DownloadRepository.php
+++ b/src/Download/Framework/Repository/DownloadRepository.php
@@ -47,7 +47,7 @@ class DownloadRepository extends ServiceEntityRepository
->andWhere('d.user = :user')
->andWhere('(d.title LIKE :term OR d.filename LIKE :term OR d.imdbId LIKE :term OR d.status LIKE :term OR d.mediaType LIKE :term)')
->orderBy('d.id', 'ASC')
- ->setParameter('statuses', ['New', 'In Progress'])
+ ->setParameter('statuses', ['New', 'In Progress', 'Paused'])
->setParameter('user', $user)
->setParameter('term', '%' . $term . '%')
->getQuery();
diff --git a/templates/components/DownloadListRow.html.twig b/templates/components/DownloadListRow.html.twig
index ee8a647..694bd75 100644
--- a/templates/components/DownloadListRow.html.twig
+++ b/templates/components/DownloadListRow.html.twig
@@ -32,8 +32,13 @@