Compare commits

...

8 Commits

Author SHA1 Message Date
Brock H Caldwell
939b059872 fix: adds download url check for monitors
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after -1m0s
2026-02-06 19:41:09 -06:00
Brock H Caldwell
f968e7e622 feat: allows configuring whether to cache torrentio results
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 13s
2026-02-06 15:23:39 -06:00
Brock H Caldwell
7958f50ff7 fix: includes missing files from last commit 2026-02-06 15:23:18 -06:00
Brock H Caldwell
f4644d40ef feat: notifies user of bad RD download (failed for copyright, etc.)
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 13s
2026-02-06 09:55:19 -06:00
Brock H Caldwell
37516c7f02 fix: closes modal when clicking dismiss button
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after -59s
2026-02-06 09:51:07 -06:00
Brock H Caldwell
c7956f5f0b fix: pushes alert dismiss button to end of div 2026-02-06 09:47:14 -06:00
Brock H Caldwell
fdf8714033 fix: supports random mysql root password 2026-02-05 18:51:06 -06:00
Brock H Caldwell
0e667fc7aa chore: updates example compose
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 14s
2026-01-25 12:57:25 -06:00
16 changed files with 262 additions and 121 deletions

3
.env
View File

@@ -68,3 +68,6 @@ SENTRY_JS_URL=
# - only include media originally # - only include media originally
# produced in this language # produced in this language
TMDB_ORIGINAL_LANGUAGE=en TMDB_ORIGINAL_LANGUAGE=en
# Cache Torrentio Results
TORRENTIO_CACHE_RESULTS=true

View File

@@ -14,5 +14,6 @@ export default class extends Controller {
"3000" "3000"
)); ));
this.element.addEventListener('mouseover', () => clearTimeout(timer)); this.element.addEventListener('mouseover', () => clearTimeout(timer));
this.element.querySelector('.modal-close').addEventListener('click', () => this.element.remove());
} }
} }

View File

@@ -52,6 +52,9 @@ parameters:
sentry.dsn: '%env(SENTRY_DSN)%' sentry.dsn: '%env(SENTRY_DSN)%'
sentry.javascript_url: '%env(SENTRY_JS_URL)%' sentry.javascript_url: '%env(SENTRY_JS_URL)%'
# Torrentio
torrentio.cache_results: '%env(bool:TORRENTIO_CACHE_RESULTS)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:

View File

@@ -1,58 +1,99 @@
# App must be served over HTTPS (requirement of Mercure) ###################
# Either serve behind an SSL terminating reverse proxy # Torsearch #
# or pass your certificates into the 'app' container. ###################
# Please omit any trailing slashes. The APP_URL is # The version of Torsearch to run. Docker will this tag.
# used to generate the Mercure URL behind the scenes. TAG=0.38.0
APP_URL="https://dev.caldwell.digital"
# The port for which the web server (app container) will
# serve the application on the host.
WEB_PORT=8004
# The host directories where your media is stored.
# If you would like to use a docker driver for a network
# share, update the compose.yml file to reflect that.
LOCAL_MOVIES_DIR="<enter movies dir>"
LOCAL_TVSHOWS_DIR="<enter tvshows dir>"
# Set the timezone you're in. This helps render monitored items correctly on the calendar
TZ=America/Chicago
###################
# Symfony #
###################
# The external URL of the application where it can be reached by a browser.
APP_URL="<enter url>"
# Requried by Symfony Framework. Feel free to change.
APP_SECRET="70169beadfbc8101c393cbfbba27a313" APP_SECRET="70169beadfbc8101c393cbfbba27a313"
# Change to 'dev' to show logs in the browser.
APP_ENV=prod APP_ENV=prod
###################
# Mercure #
###################
# Mercure is a Caddy module built into the webserver # Mercure is a Caddy module built into the webserver
# that facilitates the usage of websockets to transmit # that facilitates the usage of websockets to transmit
# real time data (download progress, etc.) # real time data (download progress, etc.)
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
MERCURE_PUBLISHER_JWT_KEY="!ChangeThisMercureHubJWTSecretKey!"
MERCURE_SUBSCRIBER_JWT_KEY="!ChangeThisMercureHubJWTSecretKey!"
###################
# Database #
###################
# Use the DATABASE_URL below to use the MariaDB container # Use the DATABASE_URL below to use the MariaDB container
# provided in the example.compose.yml file, or remove this # provided in the example.compose.yml file, or remove this
# line and fill in the details of your own MySQL/MariaDB server # line and fill in the details of your own MySQL/MariaDB server
DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-MariaDB&charset=utf8mb4" MYSQL_RANDOM_ROOT_PASSWORD=true
MYSQL_DATABASE=app
MYSQL_USER=app
MYSQL_PASSWORD=password
MYSQL_HOST=database
DATABASE_URL="mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@${MYSQL_HOST}:3306/${MYSQL_DATABASE}?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# Fill in your MySQL/MariaDB connection details
#DATABASE_URL="mysql://<mysql user>:<mysql pass>@<mysql host>:3306/<mysql db name>?serverVersion=10.6.19.2-MariaDB&charset=utf8mb4"
# Enter your Real Debrid API key ###################
# This key is never saved anywhere # Real Debrid #
# else and is passed to Torrentio ###################
# to retrieve download options # Enter your Real Debrid API key is passed to Torrentio to retrieve download options
REAL_DEBRID_KEY="" REAL_DEBRID_KEY="<enter real debrid api key>"
###################
# TMDB #
###################
# Enter your TMDB API key # Enter your TMDB API key
# This is used to provide rich search results # This is used to provide rich search results when searching
# when searching for media and rendering the # for media and rendering the Popular Movies and TV Shows section.
# Popular Movies and TV Shows section. TMDB_API="<enter tmdb api key>"
TMDB_API=""
# Use your own Redis instance or use the
# below value to use the container included ###################
# in the example compose.yml file. # Redis #
###################
# Use your own Redis instance or use the below value to use the
# container included in the example compose.yml file.
REDIS_HOST="redis://redis" REDIS_HOST="redis://redis"
### Auth ###
# Change to "oidc" to and provide the required ###################
# environment variables below to use OIDC auth. # Auth #
###################
# Options: form_login, oidc, or ldap (experimental)
# Fill the rest of the configuration based on your choice here
# No additional config is required for form_login
AUTH_METHOD=form_login AUTH_METHOD=form_login
# OIDC ### OIDC ###
OIDC_WELL_KNOWN_URL= #OIDC_WELL_KNOWN_URL=
OIDC_CLIENT_ID= #OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET= #OIDC_CLIENT_SECRET=
# Allows you to skip the login page and directly # Allows you to skip the login page and directly rely on your IdP for auth.
# rely on your IdP for auth. #OIDC_BYPASS_FORM_LOGIN=
OIDC_BYPASS_FORM_LOGIN=
### LDAP (*** Experimental! ***) ###
# LDAP Config: To use LDAP, enter the below fields # LDAP Config: To use LDAP, enter the below fields and run 'php bin/console config:set auth.method ldap'
# and run 'php bin/console config:set auth.method ldap'
# (LDAP is still in progress and not ready for use) # (LDAP is still in progress and not ready for use)
#LDAP_HOST= #LDAP_HOST=
#LDAP_PORT= #LDAP_PORT=

View File

@@ -1,78 +1,55 @@
services: services:
### The app contains the application and web server ###
app: app:
image: code.caldwell.digital/home/torsearch-app:latest image: code.caldwell.digital/home/torsearch-app:${TAG}
ports: ports:
- '8006:80' - "${WEB_PORT}:80"
env_file: env_file: .env
- .env
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on: depends_on:
database: - database
condition: service_healthy volumes:
- ${LOCAL_MOVIES_DIR}:/var/download/movies
- ${LOCAL_TVSHOWS_DIR}:/var/download/tvshows
- mercure_data:/data
- mercure_config:/config
# Downloads happen in this container. Replicate this
# container to run multiple downloads simultaneously. ### The worker handles downloads and async jobs ###
# Map your "movies" folder to /var/download/movies
# Map your "TV shows" folder to /var/download/tvshows
# If your folders are on another machine, use an NFS volume.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
worker: worker:
image: code.caldwell.digital/home/torsearch-worker:latest image: code.caldwell.digital/home/torsearch-worker:${TAG}
volumes: volumes:
- ./downloads/movies:/var/download/movies - ${LOCAL_MOVIES_DIR}:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows - ${LOCAL_TVSHOWS_DIR}:/var/download/tvshows
environment: env_file: .env
TZ: America/Chicago
command: -vv --time-limit=3600 --limit=10
env_file:
- .env
restart: always
depends_on: depends_on:
app: - app
condition: service_healthy
# This container handles the monitoring for new media. When new
# monitors are added, jobs are periodically dispatched to this ### The scheduler processes monitored media ###
# container, and the desired media is searched for and downloaded.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
scheduler: scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest image: code.caldwell.digital/home/torsearch-scheduler:${TAG}
volumes: volumes:
- ./downloads/movies:/var/download/movies - ${LOCAL_MOVIES_DIR}:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows - ${LOCAL_TVSHOWS_DIR}:/var/download/tvshows
env_file: env_file: .env
- .env
command: -vv
environment:
TZ: America/Chicago
restart: always
depends_on: depends_on:
app: - app
condition: service_healthy
#!! If using your own database, this can be omitted !!#
database: database:
image: mariadb:10.11.2 image: mariadb:10.11.2
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
environment: env_file: .env
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck: healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] test: [ "CMD", "mysqladmin", "-u", "${MYSQL_USER}", "-p", "${MYSQL_PASSWORD}" ,"ping", "-h", "localhost" ]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 10
#!! If using your own redis, this can be omitted !!#
redis: redis:
image: redis:latest image: redis:latest
volumes: volumes:
@@ -80,12 +57,6 @@ services:
command: redis-server --maxmemory 512MB command: redis-server --maxmemory 512MB
restart: unless-stopped restart: unless-stopped
# **Optional**
# Provides a simple method of viewing the database
adminer:
image: adminer
ports:
- "8081:8080"
volumes: volumes:
mysql: mysql:

View File

@@ -4,6 +4,7 @@ namespace App\Base;
use App\Base\Dto\AppVersionDto; use App\Base\Dto\AppVersionDto;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
final class ConfigResolver final class ConfigResolver
@@ -14,6 +15,7 @@ final class ConfigResolver
public function __construct( public function __construct(
private readonly DenormalizerInterface $denormalizer, private readonly DenormalizerInterface $denormalizer,
private readonly RequestStack $requestStack,
#[Autowire(param: 'app.url')] #[Autowire(param: 'app.url')]
private readonly ?string $appUrl = null, private readonly ?string $appUrl = null,
@@ -62,6 +64,9 @@ final class ConfigResolver
#[Autowire(param: 'sentry.javascript_url')] #[Autowire(param: 'sentry.javascript_url')]
private ?string $sentryJavascriptUrl = null, private ?string $sentryJavascriptUrl = null,
#[Autowire(param: 'torrentio.cache_results')]
private ?bool $torrentioCacheResults = null,
) {} ) {}
public function validate(): bool public function validate(): bool
@@ -110,6 +115,11 @@ final class ConfigResolver
return $this->authOidcBypassFormLogin; return $this->authOidcBypassFormLogin;
} }
public function isTorrentioCacheEnabled(): bool
{
return $this->torrentioCacheResults;
}
public function getAppVersion(): AppVersionDto public function getAppVersion(): AppVersionDto
{ {
$matches = []; $matches = [];

View File

@@ -24,10 +24,10 @@ readonly class Broadcaster
private LoggerInterface $logger, private LoggerInterface $logger,
) {} ) {}
public function alert(string $title, string $message, string $type = "success", bool $sendPush = false): void public function alert(string $title, string $message, string $type = "success", bool $sendPush = false, ?string $mercureAlertTopic = null): void
{ {
try { try {
$userAlertTopic = $this->requestStack->getCurrentRequest()->getSession()->get('mercure_alert_topic'); $userAlertTopic = $mercureAlertTopic ?? $this->requestStack->getCurrentRequest()->getSession()->get('mercure_alert_topic');
$update = new Update( $update = new Update(
$userAlertTopic, $userAlertTopic,
$this->renderer->render('broadcast/Alert.stream.html.twig', [ $this->renderer->render('broadcast/Alert.stream.html.twig', [
@@ -39,7 +39,7 @@ readonly class Broadcaster
); );
$this->hub->publish($update); $this->hub->publish($update);
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
// ToDo: look for better handling to get message to end user $this->logger->error('Unable to publish alert: ' . $exception->getMessage());
} }
if (true === $sendPush && in_array($this->notificationTransport, ['ntfy'])) { if (true === $sendPush && in_array($this->notificationTransport, ['ntfy'])) {

View File

@@ -17,5 +17,6 @@ class DownloadMediaCommand implements CommandInterface
public string $imdbId, public string $imdbId,
public int $userId, public int $userId,
public ?int $downloadId = null, public ?int $downloadId = null,
public ?string $mercureAlertTopic = null,
) {} ) {}
} }

View File

@@ -2,9 +2,12 @@
namespace App\Download\Action\Handler; namespace App\Download\Action\Handler;
use App\Base\Enum\MediaType;
use App\Base\Service\Broadcaster;
use App\Download\Action\Command\DownloadMediaCommand; use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Result\DownloadMediaResult; use App\Download\Action\Result\DownloadMediaResult;
use App\Download\DownloadEvents; use App\Download\DownloadEvents;
use App\Download\Framework\Entity\Download;
use App\Download\Framework\Repository\DownloadRepository; use App\Download\Framework\Repository\DownloadRepository;
use App\Download\Downloader\DownloaderInterface; use App\Download\Downloader\DownloaderInterface;
use App\EventLog\Action\Command\AddEventLogCommand; use App\EventLog\Action\Command\AddEventLogCommand;
@@ -21,8 +24,8 @@ readonly class DownloadMediaHandler implements HandlerInterface
public function __construct( public function __construct(
private MessageBusInterface $bus, private MessageBusInterface $bus,
private DownloaderInterface $downloader, private DownloaderInterface $downloader,
private DownloadRepository $downloadRepository, private DownloadRepository $downloadRepository,
private UserRepository $userRepository, private UserRepository $userRepository, private Broadcaster $broadcaster,
) {} ) {}
public function handle(CommandInterface $command): ResultInterface public function handle(CommandInterface $command): ResultInterface
@@ -49,6 +52,16 @@ readonly class DownloadMediaHandler implements HandlerInterface
$download = $this->downloadRepository->find($command->downloadId); $download = $this->downloadRepository->find($command->downloadId);
} }
try {
$this->validateDownloadUrl($download->getUrl());
} catch (\Throwable $exception) {
$download->setProgress(100);
$download->setStatus('Failed');
$this->downloadRepository->getEntityManager()->flush();
$this->sendFailedDownloadAlert($download, $command->mercureAlertTopic, $exception->getMessage());
return new DownloadMediaResult(400, $exception->getMessage());
}
try { try {
if ($download->getStatus() !== 'Paused') { if ($download->getStatus() !== 'Paused') {
$this->downloadRepository->updateStatus($download->getId(), 'In Progress'); $this->downloadRepository->updateStatus($download->getId(), 'In Progress');
@@ -77,4 +90,30 @@ readonly class DownloadMediaHandler implements HandlerInterface
)); ));
return new DownloadMediaResult(200, "Success."); return new DownloadMediaResult(200, "Success.");
} }
public function validateDownloadUrl(string $downloadUrl)
{
$badFileSizes = [
2119075, // copyright infringement
];
$badFileLocations = [
'https://torrentio.strem.fun/videos/failed_infringement_v2.mp4' => 'Removed for Copyright Infringement.',
];
$headers = get_headers($downloadUrl, true);
if (array_key_exists($headers['Location'], $badFileLocations)) {
throw new \Exception($badFileLocations[$headers['Location']]);
}
}
private function sendFailedDownloadAlert(Download $download, string $mercureAlertTopic, ?string $message = null): void
{
$this->broadcaster->alert(
title: 'Download Failed',
message: $message ?? "{$download->getTitle()} failed to download.",
type: 'warning',
mercureAlertTopic: $mercureAlertTopic
);
}
} }

View File

@@ -10,6 +10,8 @@ use OneToMany\RichBundle\Contract\InputInterface;
/** @implements InputInterface<DownloadMediaInput> */ /** @implements InputInterface<DownloadMediaInput> */
class DownloadMediaInput implements InputInterface class DownloadMediaInput implements InputInterface
{ {
public ?string $mercureAlertTopic = null;
public function __construct( public function __construct(
#[SourceRequest('url')] #[SourceRequest('url')]
public string $url, public string $url,
@@ -44,6 +46,7 @@ class DownloadMediaInput implements InputInterface
$this->imdbId, $this->imdbId,
$this->userId, $this->userId,
$this->downloadId, $this->downloadId,
$this->mercureAlertTopic,
); );
} }
} }

View File

@@ -25,6 +25,10 @@ class DownloadOptionEvaluator
return false; return false;
} }
if (false === $this->validateDownloadUrl($result->url)) {
return false;
}
return true; return true;
}); });
@@ -79,4 +83,18 @@ class DownloadOptionEvaluator
return false; return false;
} }
public function validateDownloadUrl(string $downloadUrl)
{
$badFileLocations = [
'https://torrentio.strem.fun/videos/failed_infringement_v2.mp4' => 'Removed for Copyright Infringement.',
];
$headers = get_headers($downloadUrl, true);
if (array_key_exists($headers['Location'], $badFileLocations)) {
return false;
}
return true;
}
} }

View File

@@ -15,6 +15,7 @@ use App\Download\DownloadEvents;
use App\Download\Framework\Repository\DownloadRepository; use App\Download\Framework\Repository\DownloadRepository;
use App\EventLog\Action\Command\AddEventLogCommand; use App\EventLog\Action\Command\AddEventLogCommand;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -22,9 +23,9 @@ use Symfony\Component\Routing\Attribute\Route;
class ApiController extends AbstractController class ApiController extends AbstractController
{ {
public function __construct( public function __construct(
private DownloadRepository $downloadRepository, private DownloadRepository $downloadRepository,
private MessageBusInterface $bus, private MessageBusInterface $bus,
private readonly Broadcaster $broadcaster, private readonly Broadcaster $broadcaster, private readonly RequestStack $requestStack,
) {} ) {}
#[Route('/api/download', name: 'api_download', methods: ['POST'])] #[Route('/api/download', name: 'api_download', methods: ['POST'])]
@@ -42,6 +43,7 @@ class ApiController extends AbstractController
); );
$input->downloadId = $download->getId(); $input->downloadId = $download->getId();
$input->userId = $this->getUser()->getId(); $input->userId = $this->getUser()->getId();
$input->mercureAlertTopic = $this->requestStack->getSession()->get('mercure_alert_topic');
$this->bus->dispatch(new AddEventLogCommand( $this->bus->dispatch(new AddEventLogCommand(
$this->getUser(), $this->getUser(),

View File

@@ -98,6 +98,14 @@ class DownloadRepository extends ServiceEntityRepository
return $download; return $download;
} }
public function updateProgress(int $id, int $progress): Download
{
$download = $this->find($id);
$download->setProgress($progress);
$this->getEntityManager()->flush();
return $download;
}
public function delete(int $id) public function delete(int $id)
{ {
$entity = $this->find($id); $entity = $this->find($id);
@@ -115,4 +123,16 @@ class DownloadRepository extends ServiceEntityRepository
->getQuery() ->getQuery()
->getResult(); ->getResult();
} }
public function badDownloadUrls()
{
return $this->createQueryBuilder('d')
->select('d.url')
->andWhere('d.status = :status')
->andWhere('d.progress = 100')
->setParameter('status', 'Failed')
->distinct()
->getQuery()
->getResult();
}
} }

View File

@@ -2,14 +2,25 @@
namespace App\Torrentio\Client; namespace App\Torrentio\Client;
use Aimeos\Map;
use App\Download\Framework\Repository\DownloadRepository;
use App\Torrentio\Result\ResultFactory; use App\Torrentio\Result\ResultFactory;
use App\Torrentio\Exception\TorrentioRateLimitException; use App\Torrentio\Exception\TorrentioRateLimitException;
use Psr\Log\LoggerInterface;
class Torrentio class Torrentio
{ {
private array $badDownloadUrls = [];
public function __construct( public function __construct(
private readonly HttpClient $client, private readonly HttpClient $client,
) {} private readonly DownloadRepository $downloadRepository, private readonly LoggerInterface $logger,
) {
$badDownloadUrls = $this->downloadRepository->badDownloadUrls();
$this->badDownloadUrls = Map::from($badDownloadUrls)
->map(fn ($url) => $url['url'])
->toArray();
}
public function search(string $imdbCode, string $type, bool $parseResults = true): array public function search(string $imdbCode, string $type, bool $parseResults = true): array
{ {
@@ -47,6 +58,16 @@ class Torrentio
continue; continue;
} }
$url = explode('/', $stream['url']);
$filename = urldecode(end($url));
$url[count($url) - 1] = $filename;
$url = implode('/', $url);
if (in_array($stream['url'], $this->badDownloadUrls)) {
$this->logger->warning($stream['url'] . ' was skipped because it was identified as a bad download url.');
continue;
}
if ( if (
array_key_exists('behaviorHints', $stream) && array_key_exists('behaviorHints', $stream) &&
array_key_exists('bingeGroup', $stream['behaviorHints']) array_key_exists('bingeGroup', $stream['behaviorHints'])

View File

@@ -2,6 +2,7 @@
namespace App\Torrentio\Framework\Controller; namespace App\Torrentio\Framework\Controller;
use App\Base\ConfigResolver;
use App\Base\Service\Broadcaster; use App\Base\Service\Broadcaster;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler; use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler; use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
@@ -27,6 +28,7 @@ final class WebController extends AbstractController
public function __construct( public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler, private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler, private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
private readonly ConfigResolver $configResolver,
private readonly Broadcaster $broadcaster, private readonly Broadcaster $broadcaster,
) {} ) {}
@@ -40,10 +42,14 @@ final class WebController extends AbstractController
$input->imdbId $input->imdbId
); );
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) { if (true === $this->configResolver->isTorrentioCacheEnabled()) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); $results = $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
return $this->getMovieOptionsHandler->handle($input->toCommand()); $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
}); return $this->getMovieOptionsHandler->handle($input->toCommand());
});
} else {
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
}
if ($request->headers->get('Turbo-Frame')) { if ($request->headers->get('Turbo-Frame')) {
return $this->sendFragmentResponse($results, $request); return $this->sendFragmentResponse($results, $request);
@@ -66,10 +72,14 @@ final class WebController extends AbstractController
); );
try { try {
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input) { if (true === $this->configResolver->isTorrentioCacheEnabled()) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); $results = $cache->get($cacheId, function (ItemInterface $item) use ($input) {
return $this->getTvShowOptionsHandler->handle($input->toCommand()); $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
}); return $this->getTvShowOptionsHandler->handle($input->toCommand());
});
} else {
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
}
if ($request->headers->get('Turbo-Frame')) { if ($request->headers->get('Turbo-Frame')) {
return $this->sendFragmentResponse($results, $request); return $this->sendFragmentResponse($results, $request);

View File

@@ -2,16 +2,14 @@
class="alert alert-{{ type|default('success') }}" class="alert alert-{{ type|default('success') }}"
role="alert" role="alert"
> >
<div class="flex items-center"> <div class="flex justify-between items-center">
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> <div class="flex items-center">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/> <svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
</svg> <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3> <h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3>
</div>
<twig:ux:icon name="ic:twotone-cancel" style="text-align:right" width="16.75px" height="16.75px" class="modal-close rounded-full align-end text-red-600 hover:text-red-700" /> <twig:ux:icon name="ic:twotone-cancel" width="16.75px" height="16.75px" class="modal-close rounded-full align-end text-red-600 hover:text-red-700" />
<span class="sr-only">Info</span>
</div> </div>
<div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap"> <div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap">
{{ message }} {{ message }}