Compare commits
12 Commits
9ab4f6cf57
...
v0.18.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b3506ab17 | |||
| 6e0eed8b4e | |||
| 38a5baa17e | |||
| 1d573c09e7 | |||
| 7989e2abd2 | |||
| df6c68aa46 | |||
| 6cd9a9b18e | |||
| b95e8f3794 | |||
| 8cc81fea19 | |||
| 15648e711b | |||
| f855aabd69 | |||
| 55a866170e |
@@ -10,6 +10,9 @@
|
|||||||
h2 {
|
h2 {
|
||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
}
|
}
|
||||||
|
.rounded-ms {
|
||||||
|
border-radius: 0.275rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent scrolling while dialog is open */
|
/* Prevent scrolling while dialog is open */
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ services:
|
|||||||
- mercure_config:/config
|
- mercure_config:/config
|
||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
|
TZ: America/Chicago
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -37,6 +38,8 @@ services:
|
|||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
- $PWD/var/download:/var/download
|
- $PWD/var/download:/var/download
|
||||||
tty: true
|
tty: true
|
||||||
|
environment:
|
||||||
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=10
|
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=10
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +48,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
|
environment:
|
||||||
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume scheduler_monitor -vv
|
command: php /app/bin/console messenger:consume scheduler_monitor -vv
|
||||||
tty: true
|
tty: true
|
||||||
|
|
||||||
@@ -55,6 +60,8 @@ services:
|
|||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
command: redis-server --maxmemory 512MB
|
command: redis-server --maxmemory 512MB
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: America/Chicago
|
||||||
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -64,6 +71,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mysql:/var/lib/mysql
|
- mysql:/var/lib/mysql
|
||||||
environment:
|
environment:
|
||||||
|
TZ: America/Chicago
|
||||||
MYSQL_DATABASE: app
|
MYSQL_DATABASE: app
|
||||||
MYSQL_USERNAME: app
|
MYSQL_USERNAME: app
|
||||||
MYSQL_PASSWORD: password
|
MYSQL_PASSWORD: password
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
twig:
|
twig:
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
|
date:
|
||||||
|
timezone: '%env(default:app.default.timezone:TZ)%'
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ parameters:
|
|||||||
app.cache.adapter.default: 'filesystem'
|
app.cache.adapter.default: 'filesystem'
|
||||||
app.cache.redis.host.default: 'redis://redis'
|
app.cache.redis.host.default: 'redis://redis'
|
||||||
|
|
||||||
|
# Various configs
|
||||||
|
app.default.timezone: 'America/Chicago'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
|
tty: true
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 2
|
replicas: 2
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
50
migrations/Version20250610222503.php
Normal file
50
migrations/Version20250610222503.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250610222503 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor ADD parent_id INT DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor ADD CONSTRAINT FK_E1159985727ACA70 FOREIGN KEY (parent_id) REFERENCES monitor (id)
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE INDEX IDX_E1159985727ACA70 ON monitor (parent_id)
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP FOREIGN KEY FK_E1159985727ACA70
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX IDX_E1159985727ACA70 ON monitor
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP parent_id
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,13 @@ namespace App\Monitor\Action\Handler;
|
|||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
use App\Monitor\Action\Result\MonitorMovieResult;
|
||||||
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MonitorOptionEvaluator;
|
use App\Monitor\Service\MonitorOptionEvaluator;
|
||||||
|
use App\Tmdb\Tmdb;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
@@ -27,12 +30,26 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private MonitorRepository $monitorRepository,
|
private MonitorRepository $monitorRepository,
|
||||||
|
private Tmdb $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler');
|
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler');
|
||||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||||
|
|
||||||
|
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||||
|
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::now()) {
|
||||||
|
$this->logger->info('> [MonitorTvEpisodeHandler] Episode has not aired yet, skipping for now');
|
||||||
|
return new MonitorTvEpisodeResult(
|
||||||
|
status: 'OK',
|
||||||
|
result: [
|
||||||
|
'message' => 'No change',
|
||||||
|
'monitor' => $monitor,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$monitor->setStatus('In Progress');
|
$monitor->setStatus('In Progress');
|
||||||
$this->monitorRepository->getEntityManager()->flush();
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
@@ -71,7 +88,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return new MonitorMovieResult(
|
return new MonitorTvEpisodeResult(
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
result: [
|
result: [
|
||||||
'monitor' => $monitor,
|
'monitor' => $monitor,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace App\Monitor\Action\Handler;
|
namespace App\Monitor\Action\Handler;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
|
||||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
||||||
|
use App\Monitor\Action\Result\MonitorTvSeasonResult;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MediaFiles;
|
use App\Monitor\Service\MediaFiles;
|
||||||
@@ -17,16 +17,14 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
|
||||||
|
|
||||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
/** @implements HandlerInterface<MonitorTvSeasonCommand> */
|
||||||
readonly class MonitorTvSeasonHandler implements HandlerInterface
|
readonly class MonitorTvSeasonHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private MonitorRepository $monitorRepository,
|
private MonitorRepository $monitorRepository,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private MessageBusInterface $bus,
|
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private Tmdb $tmdb,
|
private Tmdb $tmdb,
|
||||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||||
@@ -41,33 +39,42 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
|||||||
$downloadedEpisodes = $this->mediaFiles
|
$downloadedEpisodes = $this->mediaFiles
|
||||||
->getEpisodes($monitor->getTitle())
|
->getEpisodes($monitor->getTitle())
|
||||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
||||||
|
->filter(fn ($episode) =>
|
||||||
|
property_exists($episode, 'episode')
|
||||||
|
&& property_exists($episode, 'season')
|
||||||
|
&& null !== $episode->episode
|
||||||
|
&& null !== $episode->season
|
||||||
|
)
|
||||||
->rekey(fn($episode) => $episode->episode);
|
->rekey(fn($episode) => $episode->episode);
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
|
|
||||||
// Compare against list from TMDB
|
// Compare against list from TMDB
|
||||||
$episodesInSeason = Map::from(
|
$episodesInSeason = Map::from(
|
||||||
$this->tmdb->tvDetails($monitor->getTmdbId())
|
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes[$monitor->getSeason()]
|
||||||
->episodes[$monitor->getSeason()]
|
|
||||||
)->rekey(fn($episode) => $episode['episode_number']);
|
)->rekey(fn($episode) => $episode['episode_number']);
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($episodesInSeason) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($episodesInSeason) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
|
||||||
|
|
||||||
// Dispatch Episode commands for each missing Episode
|
if ($downloadedEpisodes->count() !== $episodesInSeason->count()) {
|
||||||
foreach ($episodesInSeason as $episode) {
|
// Dispatch Episode commands for each missing Episode
|
||||||
$monitorCheck = $this->monitorRepository->findOneBy([
|
foreach ($episodesInSeason as $episode) {
|
||||||
'imdbId' => $monitor->getImdbId(),
|
// Check if the episode is already downloaded
|
||||||
'title' => $monitor->getTitle(),
|
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||||
'monitorType' => 'tvepisode',
|
$this->logger->info('> [MonitorTvSeasonHandler] Episode exists for season ' . $episode['season_number'] . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||||
'season' => $monitor->getSeason(),
|
if (true === $episodeExists) {
|
||||||
'episode' => $episode['episode_number'],
|
$this->logger->info('> [MonitorTvSeasonHandler] Episode exists for title: ' . $monitor->getTitle() . ', skipping');
|
||||||
'status' => ['New', 'Active', 'In Progress']
|
continue;
|
||||||
]);
|
}
|
||||||
|
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Monitor exists for season ' . $monitor->getSeason() . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (null !== $monitorCheck ? 'YES' : 'NO'));
|
// Check for existing monitors
|
||||||
|
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||||
|
$this->logger->info('> [MonitorTvSeasonHandler] Monitor exists for season ' . $episode['season_number'] . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||||
|
if (true === $monitorExists) {
|
||||||
|
$this->logger->info('> [MonitorTvSeasonHandler] Monitor exists for title: ' . $monitor->getTitle() . ', skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!array_key_exists($episode['episode_number'], $downloadedEpisodes->toArray())
|
|
||||||
&& null === $monitorCheck
|
|
||||||
) {
|
|
||||||
$episodeMonitor = (new Monitor())
|
$episodeMonitor = (new Monitor())
|
||||||
|
->setParent($monitor)
|
||||||
->setUser($monitor->getUser())
|
->setUser($monitor->getUser())
|
||||||
->setTmdbId($monitor->getTmdbId())
|
->setTmdbId($monitor->getTmdbId())
|
||||||
->setImdbId($monitor->getImdbId())
|
->setImdbId($monitor->getImdbId())
|
||||||
@@ -88,16 +95,38 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the status to Active, so it will be re-executed.
|
||||||
|
$monitor->setStatus('Active');
|
||||||
$monitor->setLastSearch(new DateTimeImmutable());
|
$monitor->setLastSearch(new DateTimeImmutable());
|
||||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||||
$monitor->setStatus('Complete');
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return new MonitorMovieResult(
|
return new MonitorTvSeasonResult(
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
result: [
|
result: [
|
||||||
'monitor' => $monitor,
|
'monitor' => $monitor,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
|
||||||
|
{
|
||||||
|
return $downloadedEpisodes->filter(
|
||||||
|
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
|
||||||
|
&& $episode->season === $episodeInShow['season_number']
|
||||||
|
)->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function monitorExists(Monitor $monitor, array $episode): bool
|
||||||
|
{
|
||||||
|
return $this->monitorRepository->findOneBy([
|
||||||
|
'imdbId' => $monitor->getImdbId(),
|
||||||
|
'title' => $monitor->getTitle(),
|
||||||
|
'monitorType' => 'tvepisode',
|
||||||
|
'season' => $episode['season_number'],
|
||||||
|
'episode' => $episode['episode_number'],
|
||||||
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
|
]) !== null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Monitor\Action\Handler;
|
|||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvShowResult;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MediaFiles;
|
use App\Monitor\Service\MediaFiles;
|
||||||
@@ -17,7 +17,6 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
|
||||||
|
|
||||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||||
readonly class MonitorTvShowHandler implements HandlerInterface
|
readonly class MonitorTvShowHandler implements HandlerInterface
|
||||||
@@ -25,8 +24,8 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private MonitorRepository $monitorRepository,
|
private MonitorRepository $monitorRepository,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
|
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private MessageBusInterface $bus,
|
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private Tmdb $tmdb,
|
private Tmdb $tmdb,
|
||||||
) {}
|
) {}
|
||||||
@@ -39,55 +38,97 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
// Check current episodes
|
// Check current episodes
|
||||||
$downloadedEpisodes = $this->mediaFiles
|
$downloadedEpisodes = $this->mediaFiles
|
||||||
->getEpisodes($monitor->getTitle())
|
->getEpisodes($monitor->getTitle())
|
||||||
->map(fn($episode) => (object) (new PTN())->parse($episode));
|
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
||||||
|
->filter(fn ($episode) =>
|
||||||
|
property_exists($episode, 'episode')
|
||||||
|
&& property_exists($episode, 'season')
|
||||||
|
&& null !== $episode->episode
|
||||||
|
&& null !== $episode->season
|
||||||
|
)
|
||||||
|
;
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
|
|
||||||
// Compare against list from TMDB
|
// Compare against list from TMDB
|
||||||
$episodesInShow = Map::from(
|
$episodesInShow = Map::from(
|
||||||
$this->tmdb->tvDetails($monitor->getTmdbId())
|
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes
|
||||||
->episodes
|
|
||||||
)->flat(1);
|
)->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
|
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
||||||
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) {
|
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
|
||||||
$monitor = (new Monitor())
|
// Dispatch Episode commands for each missing Episode
|
||||||
|
foreach ($episodesInShow as $episode) {
|
||||||
|
// Check if the episode is already downloaded
|
||||||
|
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Episode exists for season ' . $episode['season_number'] . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||||
|
if (true === $episodeExists) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Episode exists for title: ' . $monitor->getTitle() . ', skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing monitors
|
||||||
|
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Monitor exists for season ' . $episode['season_number'] . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||||
|
if (true === $monitorExists) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Monitor exists for title: ' . $monitor->getTitle() . ', skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the monitor
|
||||||
|
$episodeMonitor = (new Monitor())
|
||||||
|
->setParent($monitor)
|
||||||
->setUser($monitor->getUser())
|
->setUser($monitor->getUser())
|
||||||
->setTmdbId($monitor->getTmdbId())
|
->setTmdbId($monitor->getTmdbId())
|
||||||
->setImdbId($monitor->getImdbId())
|
->setImdbId($monitor->getImdbId())
|
||||||
->setTitle($monitor->getTitle())
|
->setTitle($monitor->getTitle())
|
||||||
->setMonitorType('tvshow')
|
->setMonitorType('tvepisode')
|
||||||
->setSeason($episode['season_number'])
|
->setSeason($episode['season_number'])
|
||||||
->setEpisode($episode['episode_number'])
|
->setEpisode($episode['episode_number'])
|
||||||
->setCreatedAt(new DateTimeImmutable())
|
->setCreatedAt(new DateTimeImmutable())
|
||||||
->setSearchCount(0)
|
->setSearchCount(0)
|
||||||
->setStatus('New');
|
->setStatus('New');
|
||||||
|
|
||||||
$this->monitorRepository->getEntityManager()->persist($monitor);
|
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||||
$this->monitorRepository->getEntityManager()->flush();
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
$command = new MonitorTvEpisodeCommand($monitor->getId());
|
// Immediately run the monitor
|
||||||
$this->bus->dispatch($command);
|
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Dispatching MonitorTvEpisodeCommand for season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode() . ' for title: ' . $monitor->getTitle());
|
$this->monitorTvEpisodeHandler->handle($command);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Dispatching MonitorTvEpisodeCommand for season ' . $episodeMonitor->getSeason() . ' episode ' . $episodeMonitor->getEpisode() . ' for title: ' . $monitor->getTitle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the status to Active, so it will be re-executed.
|
||||||
|
$monitor->setStatus('Active');
|
||||||
$monitor->setLastSearch(new DateTimeImmutable());
|
$monitor->setLastSearch(new DateTimeImmutable());
|
||||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||||
$monitor->setStatus('Complete');
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return new MonitorTvEpisodeResult(
|
return new MonitorTvShowResult(
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
result: [
|
result: [
|
||||||
'monitor' => $monitor,
|
'monitor' => $monitor,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
|
||||||
|
{
|
||||||
|
return $downloadedEpisodes->filter(
|
||||||
|
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
|
||||||
|
&& $episode->season === $episodeInShow['season_number']
|
||||||
|
)->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function monitorExists(Monitor $monitor, array $episode): bool
|
||||||
|
{
|
||||||
|
return $this->monitorRepository->findOneBy([
|
||||||
|
'imdbId' => $monitor->getImdbId(),
|
||||||
|
'title' => $monitor->getTitle(),
|
||||||
|
'monitorType' => 'tvepisode',
|
||||||
|
'season' => $episode['season_number'],
|
||||||
|
'episode' => $episode['episode_number'],
|
||||||
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
|
]) !== null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/Monitor/Action/Result/MonitorTvSeasonResult.php
Normal file
13
src/Monitor/Action/Result/MonitorTvSeasonResult.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Monitor\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class MonitorTvSeasonResult implements ResultInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $status,
|
||||||
|
public array $result,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
13
src/Monitor/Action/Result/MonitorTvShowResult.php
Normal file
13
src/Monitor/Action/Result/MonitorTvShowResult.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Monitor\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class MonitorTvShowResult implements ResultInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $status,
|
||||||
|
public array $result,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ namespace App\Monitor\Framework\Entity;
|
|||||||
|
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||||
@@ -56,6 +58,17 @@ class Monitor
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?\DateTimeImmutable $downloadedAt = null;
|
private ?\DateTimeImmutable $downloadedAt = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||||
|
private ?self $parent = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
||||||
|
private Collection $children;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->children = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -204,4 +217,46 @@ class Monitor
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getParent(): ?self
|
||||||
|
{
|
||||||
|
return $this->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParent(?self $parent): static
|
||||||
|
{
|
||||||
|
$this->parent = $parent;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, self>
|
||||||
|
*/
|
||||||
|
public function getChildren(): Collection
|
||||||
|
{
|
||||||
|
return $this->children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addChild(self $child): static
|
||||||
|
{
|
||||||
|
if (!$this->children->contains($child)) {
|
||||||
|
$this->children->add($child);
|
||||||
|
$child->setParent($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeChild(self $child): static
|
||||||
|
{
|
||||||
|
if ($this->children->removeElement($child)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($child->getParent() === $this) {
|
||||||
|
$child->setParent(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
||||||
|
|
||||||
#[AsCronTask('* * * * *', schedule: 'monitor')]
|
#[AsCronTask('0 * * * *', schedule: 'monitor')]
|
||||||
class MonitorDispatcher
|
class MonitorDispatcher
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class MediaFiles
|
|||||||
foreach ($existingEpisodes as $episode) {
|
foreach ($existingEpisodes as $episode) {
|
||||||
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
||||||
|
|
||||||
if (!property_exists($episode, 'season') || !property_exists($episode, 'episode')) {
|
if (!property_exists($ptn, 'season') || !property_exists($ptn, 'episode')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ final class MonitorList extends AbstractController
|
|||||||
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
->andWhere('m.status IN (:statuses)')
|
->andWhere('m.status IN (:statuses)')
|
||||||
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
->setParameter('statuses', ['New', 'In Progress'])
|
->setParameter('statuses', ['New', 'In Progress', 'Active'])
|
||||||
->setParameter('term', '%'.$this->term.'%')
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
->orderBy('m.id', 'DESC')
|
->orderBy('m.id', 'DESC')
|
||||||
->getQuery()
|
->getQuery()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<li {{ attributes }} id="alert_{{ alert_id }}" class="
|
<li {{ attributes }} id="alert_{{ alert_id }}" class="
|
||||||
text-white bg-green-950 text-sm min-w-[250px]
|
text-white bg-green-950 text-sm min-w-[250px]
|
||||||
hover:bg-green-900 border border-green-500 px-4 py-3
|
hover:bg-green-900 border border-green-500 px-4 py-3
|
||||||
rounded-md z-40"
|
rounded-md"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% if this.isWidget == false %}
|
{% if this.isWidget == false %}
|
||||||
<div class="flex flex-row mb-2 justify-end">
|
<div class="flex flex-row mb-2 justify-end">
|
||||||
<twig:DownloadSearch search_path="app_search" placeholder="Find one of your downloads..." />
|
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} download..." />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<tr{{ attributes }} id="ad_download_{{ download.id }}">
|
<tr{{ attributes }} class="hover:bg-gray-200" id="ad_download_{{ download.id }}">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||||
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
|
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
|
||||||
class="mr-1 hover:underline rounded-md"
|
class="mr-1 hover:underline rounded-md"
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
focus:bg-green-700 active:bg-green-700 hover:bg-green-700
|
focus:bg-green-700 active:bg-green-700 hover:bg-green-700
|
||||||
|
|
||||||
text-white bg-green-600 text-xs
|
text-white bg-green-600 text-xs
|
||||||
rounded-sm
|
rounded-ms bg-opacity-80
|
||||||
bg-opacity-80
|
|
||||||
"
|
"
|
||||||
onclick="return false;"
|
onclick="return false;"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="absolute top-10 right-10">
|
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="fixed z-40 top-10 right-10">
|
||||||
<div >
|
<div class="z-40">
|
||||||
<ul id="alert_list">
|
<ul id="alert_list">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div{{ attributes.defaults(stimulus_controller('monitor_list')) }}>
|
<div{{ attributes.defaults(stimulus_controller('monitor_list')) }}>
|
||||||
{% if this.isWidget == false %}
|
{% if this.isWidget == false %}
|
||||||
<div class="flex flex-row mb-2 justify-end">
|
<div class="flex flex-row mb-2 justify-end">
|
||||||
<twig:DownloadSearch search_path="app_search" placeholder="Find one of your monitors..." />
|
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table id="monitor_list" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Monitor\\Framework\\Entity\\Monitor') }}>
|
<table id="monitor_list" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Monitor\\Framework\\Entity\\Monitor') }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<tr{{ attributes }} id="monitor_{{ monitor.id }}">
|
<tr{{ attributes }} id="monitor_{{ monitor.id }}" class="hover:bg-gray-200">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
||||||
{{ monitor.title }}
|
{{ monitor.title }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user