wip-feat: dispatches monitor commands for episodes, seasons, & shows

This commit is contained in:
2025-05-06 00:00:45 -05:00
parent 9166b4bbc8
commit 527adb73c1
33 changed files with 795 additions and 147 deletions

View File

@@ -3,11 +3,18 @@
namespace App\Controller;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Handler\AddMovieMonitorHandler;
use App\Download\Action\Command\MonitorTvSeasonCommand;
use App\Download\Action\Command\MonitorTvShowCommand;
use App\Download\Action\Handler\MonitorMovieHandler;
use App\Download\Action\Input\AddMovieMonitorInput;
use App\Download\Action\Handler\MonitorTvSeasonHandler;
use App\Download\Action\Handler\MonitorTvShowHandler;
use App\Download\Action\Input\DownloadMediaInput;
use App\Download\Framework\Entity\Monitor;
use App\Download\Framework\Repository\DownloadRepository;
use App\Download\Framework\Repository\MonitorRepository;
use App\Download\Service\MediaFiles;
use DateTimeImmutable;
use Nihilarr\PTN;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -18,13 +25,28 @@ class DownloadController extends AbstractController
public function __construct(
private DownloadRepository $downloadRepository,
private MessageBusInterface $bus,
private readonly MonitorRepository $monitorRepository,
) {}
#[Route('/test', name: 'app_test')]
public function test(
MonitorMovieHandler $handler,
MonitorTvShowHandler $handler,
) {
$command = new MonitorMovieCommand(41);
$monitor = (new Monitor())
->setUser($this->getUser())
->setTmdbId('95396')
->setImdbId('tt11280740')
->setTitle('Severance')
->setMonitorType('tvshow')
->setSeason(1)
->setEpisode(null)
->setCreatedAt(new DateTimeImmutable())
->setSearchCount(0)
->setStatus('New');
$this->monitorRepository->getEntityManager()->persist($monitor);
$this->monitorRepository->getEntityManager()->flush();
$command = new MonitorTvShowCommand($monitor->getId());
$handler->handle($command);
return $this->json([
'status' => 200,
@@ -53,16 +75,4 @@ class DownloadController extends AbstractController
return $this->json(['status' => 200, 'message' => 'Added to Queue']);
}
#[Route('/monitor/movies/{tmdbId}/{imdbId}/{title}', name: 'app_add_movie_monitor', methods: ['GET', 'POST'])]
public function addMonitor(
AddMovieMonitorInput $input,
AddMovieMonitorHandler $handler,
) {
$handler->handle($input->toCommand());
return $this->json([
'status' => 200,
'message' => $input
]);
}
}

View File

@@ -4,9 +4,9 @@ namespace App\Controller;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Handler\MonitorMovieHandler;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Entity\Monitor;
use App\Download\Framework\Repository\DownloadRepository;
use App\Download\Framework\Repository\MovieMonitorRepository;
use App\Download\Framework\Repository\MonitorRepository;
use App\Tmdb\Tmdb;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

View File

@@ -2,15 +2,18 @@
namespace App\Download\Action\Command;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Entity\Monitor;
use OneToMany\RichBundle\Contract\CommandInterface;
class AddMovieMonitorCommand implements CommandInterface
class AddMonitorCommand implements CommandInterface
{
public function __construct(
public string $userEmail,
public string $title,
public string $imdbId,
public string $tmdbId,
public string $monitorType,
public ?int $season,
public ?int $episode,
) {}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Download\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class MonitorTvEpisodeCommand implements CommandInterface
{
public function __construct(
public int $movieMonitorId,
) {}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Download\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class MonitorTvSeasonCommand implements CommandInterface
{
public function __construct(
public int $monitorId,
) {}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Download\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class MonitorTvShowCommand implements CommandInterface
{
public function __construct(
public int $monitorId,
) {}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Download\Action\Handler;
use App\Download\Action\Command\AddMonitorCommand;
use App\Download\Action\Result\AddMonitorResult;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Framework\Entity\Monitor;
use App\Download\Framework\Repository\MonitorRepository;
use App\User\Framework\Repository\UserRepository;
use DateTimeImmutable;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements HandlerInterface<AddMonitorCommand> */
readonly class AddMonitorHandler implements HandlerInterface
{
public function __construct(
private MonitorRepository $movieMonitorRepository,
private UserRepository $userRepository,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$user = $this->userRepository->findOneBy(['email' => $command->userEmail]);
$monitor = (new Monitor())
->setUser($user)
->setTmdbId($command->tmdbId)
->setImdbId($command->imdbId)
->setTitle($command->title)
->setMonitorType($command->monitorType)
->setSeason($command->season)
->setEpisode($command->episode)
->setCreatedAt(new DateTimeImmutable())
->setSearchCount(0)
->setStatus('New');
$this->movieMonitorRepository->getEntityManager()->persist($monitor);
$this->movieMonitorRepository->getEntityManager()->flush();
return new AddMonitorResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace App\Download\Action\Handler;
use App\Download\Action\Command\AddMovieMonitorCommand;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Repository\MovieMonitorRepository;
use App\User\Framework\Repository\UserRepository;
use DateTimeImmutable;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements HandlerInterface<AddMovieMonitorCommand> */
readonly class AddMovieMonitorHandler implements HandlerInterface
{
public function __construct(
private MovieMonitorRepository $movieMonitorRepository,
private UserRepository $userRepository,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$user = $this->userRepository->findOneBy(['email' => $command->userEmail]);
$monitor = new MovieMonitor();
$monitor->setTmdbId($command->tmdbId);
$monitor->setImdbId($command->imdbId);
$monitor->setTitle($command->title);
$monitor->setUser($user);
$monitor->setCreatedAt(new DateTimeImmutable());
$monitor->setSearchCount(0);
$monitor->setStatus('New');
$this->movieMonitorRepository->getEntityManager()->persist($monitor);
$this->movieMonitorRepository->getEntityManager()->flush();
return new MonitorMovieResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Download\Action\Handler;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Framework\Repository\MovieMonitorRepository;
use App\Download\Framework\Repository\MonitorRepository;
use App\Download\Service\MonitorOptionEvaluator;
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
@@ -21,7 +21,7 @@ use Symfony\Component\Messenger\MessageBusInterface;
readonly class MonitorMovieHandler implements HandlerInterface
{
public function __construct(
private MovieMonitorRepository $movieMonitorRepository,
private MonitorRepository $movieMonitorRepository,
private GetMovieOptionsHandler $getMovieOptionsHandler,
private MonitorOptionEvaluator $monitorOptionEvaluator,
private EntityManagerInterface $entityManager,
@@ -36,7 +36,6 @@ readonly class MonitorMovieHandler implements HandlerInterface
$monitor->setStatus('In Progress');
$this->logger->info('> [MonitorMovieHandler] Searching for "' . $monitor->getTitle() . '" download options');
$results = $this->getMovieOptionsHandler->handle(
new GetMovieOptionsCommand($monitor->getTmdbId(), $monitor->getImdbId())
);

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Download\Action\Handler;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Framework\Repository\MonitorRepository;
use App\Download\Service\MonitorOptionEvaluator;
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorTvEpisodeHandler implements HandlerInterface
{
public function __construct(
private MonitorRepository $movieMonitorRepository,
private GetMovieOptionsHandler $getMovieOptionsHandler,
private MonitorOptionEvaluator $monitorOptionEvaluator,
private EntityManagerInterface $entityManager,
private MessageBusInterface $bus,
private LoggerInterface $logger,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler');
$monitor = $this->movieMonitorRepository->find($command->movieMonitorId);
$monitor->setStatus('In Progress');
$this->logger->info('> [MonitorTvEpisodeHandler] Searching for "' . $monitor->getTitle() . '" download options');
$results = $this->getMovieOptionsHandler->handle(
new GetTvShowOptionsCommand(
$monitor->getTmdbId(),
$monitor->getImdbId(),
$monitor->getSeason(),
$monitor->getEpisode())
);
$this->logger->info('> [MonitorTvEpisodeHandler] Found ' . count($results->results) . ' download options');
$result = $this->monitorOptionEvaluator->evaluateOptions($monitor, $results->results);
if (null !== $result) {
$this->logger->info('> [MonitorTvEpisodeHandler] 1 result found: dispatching DownloadMediaCommand for "' . $result->title . '"');
$this->bus->dispatch(new DownloadMediaCommand(
$result->url,
$monitor->getTitle(),
$result->filename,
'movies',
$monitor->getImdbId(),
));
$monitor->setStatus('Complete');
$monitor->setDownloadedAt(new DateTimeImmutable());
}
$monitor->setLastSearch(new DateTimeImmutable());
$monitor->setSearchCount($monitor->getSearchCount() + 1);
$this->entityManager->flush();
return new MonitorMovieResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Download\Action\Handler;
use Aimeos\Map;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Command\MonitorTvEpisodeCommand;
use App\Download\Action\Command\MonitorTvSeasonCommand;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Framework\Entity\Monitor;
use App\Download\Framework\Repository\MonitorRepository;
use App\Download\Service\MediaFiles;
use App\Download\Service\MonitorOptionEvaluator;
use App\Tmdb\Tmdb;
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Nihilarr\PTN;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorTvSeasonHandler implements HandlerInterface
{
public function __construct(
private MonitorRepository $monitorRepository,
private GetMovieOptionsHandler $getMovieOptionsHandler,
private MonitorOptionEvaluator $monitorOptionEvaluator,
private EntityManagerInterface $entityManager,
private MediaFiles $mediaFiles,
private MessageBusInterface $bus,
private LoggerInterface $logger,
private Tmdb $tmdb,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$this->logger->info('> [MonitorTvSeasonHandler] Executing MonitorTvSeasonHandler');
$monitor = $this->monitorRepository->find($command->monitorId);
// Check current episodes
$downloadedEpisodes = $this->mediaFiles
->getEpisodes($monitor->getTitle())
->map(fn($episode) => (object) (new PTN())->parse($episode))
->rekey(fn($episode) => $episode->episode);
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
// Compare against list from TMDB
$episodesInSeason = Map::from(
$this->tmdb->tvDetails($monitor->getTmdbId())
->episodes[$monitor->getSeason()]
)->rekey(fn($episode) => $episode['episode_number']);
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($episodesInSeason) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
// Dispatch Episode commands for each missing Episode
foreach ($episodesInSeason as $episode) {
if (!array_key_exists($episode['episode_number'], $downloadedEpisodes->toArray())) {
$monitor = (new Monitor())
->setUser($monitor->getUser())
->setTmdbId($monitor->getTmdbId())
->setImdbId($monitor->getImdbId())
->setTitle($monitor->getTitle())
->setMonitorType('tvseason')
->setSeason($monitor->getSeason())
->setEpisode($episode['episode_number'])
->setCreatedAt(new DateTimeImmutable())
->setSearchCount(0)
->setStatus('New');
$this->monitorRepository->getEntityManager()->persist($monitor);
$this->monitorRepository->getEntityManager()->flush();
$command = new MonitorTvEpisodeCommand($monitor->getId());
$this->bus->dispatch($command);
$this->logger->info('> [MonitorTvSeasonHandler] Dispatching MonitorTvEpisodeCommand for season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode() . ' for title: ' . $monitor->getTitle());
}
}
$monitor->setLastSearch(new DateTimeImmutable());
$monitor->setSearchCount($monitor->getSearchCount() + 1);
$this->entityManager->flush();
return new MonitorMovieResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Download\Action\Handler;
use Aimeos\Map;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Action\Command\MonitorTvEpisodeCommand;
use App\Download\Action\Command\MonitorTvSeasonCommand;
use App\Download\Action\Result\MonitorMovieResult;
use App\Download\Action\Result\MonitorTvEpisodeResult;
use App\Download\Framework\Entity\Monitor;
use App\Download\Framework\Repository\MonitorRepository;
use App\Download\Service\MediaFiles;
use App\Download\Service\MonitorOptionEvaluator;
use App\Tmdb\Tmdb;
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Nihilarr\PTN;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorTvShowHandler implements HandlerInterface
{
public function __construct(
private MonitorRepository $monitorRepository,
private EntityManagerInterface $entityManager,
private MediaFiles $mediaFiles,
private MessageBusInterface $bus,
private LoggerInterface $logger,
private Tmdb $tmdb,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
$monitor = $this->monitorRepository->find($command->monitorId);
// Check current episodes
$downloadedEpisodes = $this->mediaFiles
->getEpisodes($monitor->getTitle())
->map(fn($episode) => (object) (new PTN())->parse($episode));
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
// Compare against list from TMDB
$episodesInShow = Map::from(
$this->tmdb->tvDetails($monitor->getTmdbId())
->episodes
)->flat(1);
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
// Dispatch Episode commands for each missing Episode
foreach ($episodesInShow as $episode) {
$episodeAlreadyDownloaded = $downloadedEpisodes->find(
fn($ep) => $ep->episode === $episode['episode_number'] && $ep->season === $episode['season_number']
);
$episodeAlreadyDownloaded = !is_null($episodeAlreadyDownloaded);
if (false === $episodeAlreadyDownloaded) {
$monitor = (new Monitor())
->setUser($monitor->getUser())
->setTmdbId($monitor->getTmdbId())
->setImdbId($monitor->getImdbId())
->setTitle($monitor->getTitle())
->setMonitorType('tvshow')
->setSeason($episode['season_number'])
->setEpisode($episode['episode_number'])
->setCreatedAt(new DateTimeImmutable())
->setSearchCount(0)
->setStatus('New');
$this->monitorRepository->getEntityManager()->persist($monitor);
$this->monitorRepository->getEntityManager()->flush();
$command = new MonitorTvEpisodeCommand($monitor->getId());
$this->bus->dispatch($command);
$this->logger->info('> [MonitorTvShowHandler] Dispatching MonitorTvEpisodeCommand for season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode() . ' for title: ' . $monitor->getTitle());
}
}
$monitor->setLastSearch(new DateTimeImmutable());
$monitor->setSearchCount($monitor->getSearchCount() + 1);
$this->entityManager->flush();
return new MonitorTvEpisodeResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Download\Action\Input;
use App\Download\Action\Command\AddMonitorCommand;
use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Attribute\SourceSecurity;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
class AddMonitorInput implements InputInterface
{
public function __construct(
#[SourceSecurity]
public string $userEmail,
#[SourceRequest('tmdbId')]
public string $tmdbId,
#[SourceRequest('imdbId')]
public string $imdbId,
#[SourceRequest('title')]
public string $title,
#[SourceRequest('monitorType')]
public string $monitorType,
#[SourceRequest('season', nullify: true)]
public ?int $season,
#[SourceRequest('episode', nullify: true)]
public ?int $episode,
) {}
public function toCommand(): CommandInterface
{
return new AddMonitorCommand(
$this->userEmail,
$this->title,
$this->imdbId,
$this->tmdbId,
$this->monitorType,
$this->season,
$this->episode,
);
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace App\Download\Action\Input;
use App\Download\Action\Command\AddMovieMonitorCommand;
use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Attribute\SourceSecurity;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
class AddMovieMonitorInput implements InputInterface
{
public function __construct(
#[SourceSecurity]
public string $userEmail,
#[SourceRoute('tmdbId')]
public string $tmdbId,
#[SourceRoute('imdbId')]
public string $imdbId,
#[SourceRoute('title')]
public string $title,
) {}
public function toCommand(): CommandInterface
{
return new AddMovieMonitorCommand($this->userEmail, $this->title, $this->imdbId, $this->tmdbId);
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Download\Action\Result;
use OneToMany\RichBundle\Contract\ResultInterface;
class AddMovieMonitorResult implements ResultInterface
class AddMonitorResult implements ResultInterface
{
public function __construct(
public string $status,

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Download\Action\Result;
use OneToMany\RichBundle\Contract\ResultInterface;
class MonitorTvEpisodeResult implements ResultInterface
{
public function __construct(
public string $status,
public array $result,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Download\Framework\Controller;
use App\Download\Action\Handler\AddMonitorHandler;
use App\Download\Action\Input\AddMonitorInput;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
class ApiController extends AbstractController
{
#[Route('/monitor', name: 'app_add_movie_monitor', methods: ['POST'])]
public function addMonitor(
AddMonitorInput $input,
AddMonitorHandler $handler,
) {
$response = $handler->handle($input->toCommand());
return $this->json([
'status' => 200,
'message' => $response
]);
}
}

View File

@@ -2,13 +2,13 @@
namespace App\Download\Framework\Entity;
use App\Download\Framework\Repository\MovieMonitorRepository;
use App\Download\Framework\Repository\MonitorRepository;
use App\User\Framework\Entity\User;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MovieMonitorRepository::class)]
class MovieMonitor
#[ORM\Entity(repositoryClass: MonitorRepository::class)]
class Monitor
{
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -28,6 +28,15 @@ class MovieMonitor
#[ORM\Column(length: 255)]
private ?string $tmdbId = null;
#[ORM\Column(length: 255)]
private ?string $monitorType = null;
#[ORM\Column(nullable: true)]
private ?int $season = null;
#[ORM\Column(nullable: true)]
private ?int $episode = null;
#[ORM\Column(length: 255)]
private ?string $status = null;
@@ -155,4 +164,40 @@ class MovieMonitor
return $this;
}
public function getMonitorType(): ?string
{
return $this->monitorType;
}
public function setMonitorType(string $monitorType): static
{
$this->monitorType = $monitorType;
return $this;
}
public function getSeason(): ?int
{
return $this->season;
}
public function setSeason(?int $season): static
{
$this->season = $season;
return $this;
}
public function getEpisode(): ?int
{
return $this->episode;
}
public function setEpisode(?int $episode): static
{
$this->episode = $episode;
return $this;
}
}

View File

@@ -2,18 +2,18 @@
namespace App\Download\Framework\Repository;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Entity\Monitor;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MovieMonitor>
* @extends ServiceEntityRepository<Monitor>
*/
class MovieMonitorRepository extends ServiceEntityRepository
class MonitorRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MovieMonitor::class);
parent::__construct($registry, Monitor::class);
}
// /**

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Download\Framework\Scheduler;
use App\Download\Action\Handler\MonitorMovieHandler;
use App\Download\Action\Handler\MonitorTvSeasonHandler;
use App\Download\Framework\Repository\MonitorRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCronTask('* * * * *', schedule: 'monitor')]
class MonitorDispatcher
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly MonitorRepository $monitorRepository,
private readonly MessageBusInterface $bus,
) {}
public function __invoke() {
$this->logger->info('[MonitorDispatcher] Executing MovieMonitorDispatcher');
$monitorHandlers = [
'movie' => MonitorMovieHandler::class,
'tvepisode' => MonitorTvSeasonHandler::class,
'tvseason' => MonitorTvSeasonHandler::class,
'tvshow' => MonitorTvSeasonHandler::class,
];
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'In Progress']]);
foreach ($monitors as $monitor) {
$handler = $monitorHandlers[$monitor->getMonitorType()];
$this->logger->info('[MonitorDispatcher] Dispatching ' . $handler . ' for ' . $monitor->getTitle());
$this->bus->dispatch(new $handler($monitor->getId()));
}
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Download\Framework\Scheduler;
use App\Download\Action\Command\MonitorMovieCommand;
use App\Download\Framework\Repository\MovieMonitorRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCronTask('* * * * *', schedule: 'movie_monitor')]
class MovieMonitorDispatcher
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly MovieMonitorRepository $movieMonitorRepository,
private readonly MessageBusInterface $bus,
) {}
public function __invoke() {
$this->logger->info('[MovieMonitorDispatcher] Executing MovieMonitorDispatcher');
$monitors = $this->movieMonitorRepository->findBy(['status' => ['New', 'In Progress']]);
foreach ($monitors as $monitor) {
$this->logger->info('[MovieMonitorDispatcher] Dispatching MovieMonitorCommand for ' . $monitor->getTitle());
$this->bus->dispatch(new MonitorMovieCommand($monitor->getId()));
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Download\Service;
use Aimeos\Map;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Finder\Finder;
class MediaFiles
{
private Finder $finder;
private string $moviesPath;
private string $tvShowsPath;
public function __construct(
#[Autowire(param: 'media.movies_path')]
string $moviesPath,
#[Autowire(param: 'media.tvshows_path')]
string $tvShowsPath,
) {
$this->finder = new Finder();
$this->moviesPath = $moviesPath;
$this->tvShowsPath = $tvShowsPath;
}
public function getMoviesPath(): string
{
return $this->moviesPath;
}
public function getTvShowsPath(): string
{
return $this->tvShowsPath;
}
public function getMovieDirs(): Map
{
$results = [];
foreach ($this->finder->in($this->moviesPath)->directories() as $file) {
$results[] = $file;
}
return Map::from($results);
}
public function getTvShowDirs(): Map
{
$results = [];
foreach ($this->finder->in($this->tvShowsPath)->directories() as $file) {
$results[] = $file;
}
return Map::from($results);
}
public function getEpisodes(string $path, bool $onlyFilenames = true): Map
{
if (!str_starts_with($path, $this->tvShowsPath)) {
$path = $this->tvShowsPath . DIRECTORY_SEPARATOR . $path;
}
$results = [];
foreach ($this->finder->in($path)->files() as $file) {
if ($onlyFilenames) {
$results[] = $file->getRelativePathname();
} else {
$results[] = $file;
}
}
return Map::from($results);
}
}

View File

@@ -3,18 +3,18 @@
namespace App\Download\Service;
use Aimeos\Map;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Entity\Monitor;
use App\Torrentio\Result\TorrentioResult;
class MonitorOptionEvaluator
{
/**
* @param MovieMonitor $monitor
* @param Monitor $monitor
* @param TorrentioResult[] $results
* @return TorrentioResult|null
* @throws \Throwable
*/
public function evaluateOptions(MovieMonitor $monitor, array $results): ?TorrentioResult
public function evaluateOptions(Monitor $monitor, array $results): ?TorrentioResult
{
$sizeLow = 500;
$sizeHigh = 4096;

View File

@@ -2,7 +2,7 @@
namespace App;
use App\Download\Framework\Scheduler\MovieMonitorDispatcher;
use App\Download\Framework\Scheduler\MonitorDispatcher;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

View File

@@ -3,7 +3,7 @@
namespace App\User\Framework\Entity;
use Aimeos\Map;
use App\Download\Framework\Entity\MovieMonitor;
use App\Download\Framework\Entity\Monitor;
use App\User\Framework\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -41,9 +41,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private Collection $userPreferences;
/**
* @var Collection<int, MovieMonitor>
* @var Collection<int, Monitor>
*/
#[ORM\OneToMany(targetEntity: MovieMonitor::class, mappedBy: 'user', orphanRemoval: true)]
#[ORM\OneToMany(targetEntity: Monitor::class, mappedBy: 'user', orphanRemoval: true)]
private Collection $yes;
public function __construct()
@@ -213,14 +213,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @return Collection<int, MovieMonitor>
* @return Collection<int, Monitor>
*/
public function getYes(): Collection
{
return $this->yes;
}
public function addYe(MovieMonitor $ye): static
public function addYe(Monitor $ye): static
{
if (!$this->yes->contains($ye)) {
$this->yes->add($ye);
@@ -230,7 +230,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
public function removeYe(MovieMonitor $ye): static
public function removeYe(Monitor $ye): static
{
if ($this->yes->removeElement($ye)) {
// set the owning side to null (unless already changed)