Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0f1473037 | |||
| fc797a3a0f | |||
| b8b71fa5b3 | |||
| 662e2600f6 | |||
| aa042e8275 | |||
| 57498b1abf | |||
| fed1e1e122 | |||
| 9eef567974 | |||
| 070723581a | |||
| f3a5c2012e | |||
| 5581a82554 | |||
| 3703272f59 |
@@ -46,6 +46,7 @@
|
|||||||
"symfony/messenger": "7.3.*",
|
"symfony/messenger": "7.3.*",
|
||||||
"symfony/notifier": "7.3.*",
|
"symfony/notifier": "7.3.*",
|
||||||
"symfony/ntfy-notifier": "7.3.*",
|
"symfony/ntfy-notifier": "7.3.*",
|
||||||
|
"symfony/object-mapper": "7.3.*",
|
||||||
"symfony/runtime": "7.3.*",
|
"symfony/runtime": "7.3.*",
|
||||||
"symfony/scheduler": "7.3.*",
|
"symfony/scheduler": "7.3.*",
|
||||||
"symfony/security-bundle": "7.3.*",
|
"symfony/security-bundle": "7.3.*",
|
||||||
|
|||||||
75
composer.lock
generated
75
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "9ffd10f98137e8975de2c04ac2412ed5",
|
"content-hash": "e055bbbbe5836c92bb147b6dbb1d1d46",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -7783,6 +7783,79 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-02-13T10:27:54+00:00"
|
"time": "2025-02-13T10:27:54+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/object-mapper",
|
||||||
|
"version": "v7.3.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/object-mapper.git",
|
||||||
|
"reference": "f7f9833d9fcc8361239c1dae5495aa9e43ece0b5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/object-mapper/zipball/f7f9833d9fcc8361239c1dae5495aa9e43ece0b5",
|
||||||
|
"reference": "f7f9833d9fcc8361239c1dae5495aa9e43ece0b5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/container": "^2.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/property-access": "<7.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/property-access": "^7.2",
|
||||||
|
"symfony/var-exporter": "^7.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\ObjectMapper\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides a way to map an object to another object",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/object-mapper/tree/v7.3.3"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-13T14:03:15+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/options-resolver",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ security:
|
|||||||
# Easy way to control access for large sections of your site
|
# Easy way to control access for large sections of your site
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
|
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ services:
|
|||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Session
|
# Session
|
||||||
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
||||||
arguments:
|
arguments:
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
FROM dunglas/frankenphp:php8.4-alpine
|
ARG APP_VERSION
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
FROM code.caldwell.digital/torsearch/torsearch-app:${APP_VERSION}
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||||
|
|
||||||
|
|||||||
47
migrations/Version20250831013403.php
Normal file
47
migrations/Version20250831013403.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?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 Version20250831013403 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'
|
||||||
|
DROP TABLE sessions
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -6,4 +6,5 @@ enum MediaType: string
|
|||||||
{
|
{
|
||||||
case Movie = 'movies';
|
case Movie = 'movies';
|
||||||
case TvShow = 'tvshows';
|
case TvShow = 'tvshows';
|
||||||
|
case TvEpisode = 'tvepisode';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ class SeedDatabaseCommand extends Command
|
|||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'type' => 'download'
|
'type' => 'download'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'id' => 'enable_ical_up_ep',
|
||||||
|
'name' => 'Enable a publicly available iCal calendar?',
|
||||||
|
'description' => 'Enable a publicly accessible iCal URL for your upcoming episodes.',
|
||||||
|
'enabled' => false,
|
||||||
|
'type' => 'calendar'
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Base\Framework\Controller;
|
namespace App\Base\Framework\Controller;
|
||||||
|
|
||||||
use App\Monitor\Action\Command\MonitorTvShowCommand;
|
|
||||||
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use App\User\Framework\Entity\User;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -16,20 +14,17 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
final class IndexController extends AbstractController
|
final class IndexController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly TmdbClient $tmdb,
|
||||||
private readonly MonitorTvShowHandler $monitorTvShowHandler,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/', name: 'app_index')]
|
#[Route('/', name: 'app_index')]
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
|
||||||
$user = $this->getUser();
|
|
||||||
return $this->render('index/index.html.twig', [
|
return $this->render('index/index.html.twig', [
|
||||||
'active_downloads' => $this->getUser()->getActiveDownloads(),
|
'active_downloads' => $this->getUser()->getActiveDownloads(),
|
||||||
'recent_downloads' => $this->getUser()->getDownloads(),
|
'recent_downloads' => $this->getUser()->getDownloads(),
|
||||||
'popular_movies' => $this->tmdb->popularMovies(1, 6),
|
'popular_movies' => $this->tmdb->popularMovies(),
|
||||||
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
|
'popular_tvshows' => $this->tmdb->popularTvShows(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ class ImdbMatcher
|
|||||||
{
|
{
|
||||||
public static function isMatch(string $imdbId): bool
|
public static function isMatch(string $imdbId): bool
|
||||||
{
|
{
|
||||||
return preg_match('/^tt\d{7}$/', $imdbId);
|
return preg_match('/^tt\d{7,20}$/', $imdbId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
namespace App\Download\Action\Handler;
|
namespace App\Download\Action\Handler;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Download\Action\Command\DownloadSeasonCommand;
|
use App\Download\Action\Command\DownloadSeasonCommand;
|
||||||
use App\Download\Action\Result\DownloadMediaResult;
|
use App\Download\Action\Result\DownloadMediaResult;
|
||||||
use App\Download\Action\Result\DownloadSeasonResult;
|
use App\Download\Action\Result\DownloadSeasonResult;
|
||||||
use App\Download\DownloadOptionEvaluator;
|
use App\Download\DownloadOptionEvaluator;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
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 App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
@@ -27,7 +28,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private Tmdb $tmdb,
|
private TmdbClient $tmdb,
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
@@ -36,7 +37,8 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$series = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$series = $this->tmdb->tvshowDetails($command->imdbId);
|
||||||
|
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] Executing DownloadTvSeasonHandler for "' . $series->title . '" season ' . $command->season);
|
$this->logger->info('> [DownloadTvSeasonHandler] Executing DownloadTvSeasonHandler for "' . $series->title . '" season ' . $command->season);
|
||||||
|
|
||||||
$episodesInSeason = Map::from($series->episodes[$command->season]);
|
$episodesInSeason = Map::from($series->episodes[$command->season]);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use App\Download\Framework\Repository\DownloadRepository;
|
|||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
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 App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
@@ -32,7 +32,7 @@ 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,
|
private TmdbClient $tmdb,
|
||||||
private DownloadRepository $downloadRepository,
|
private DownloadRepository $downloadRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
|||||||
use App\Monitor\Action\Result\MonitorTvSeasonResult;
|
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\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use App\Base\Util\PTN;
|
use App\Base\Util\PTN;
|
||||||
@@ -26,7 +26,7 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
|||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private Tmdb $tmdb,
|
private TmdbClient $tmdb,
|
||||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
// Compare against list from TMDB
|
// Compare against list from TMDB
|
||||||
$episodesInSeason = Map::from(
|
$episodesInSeason = Map::from(
|
||||||
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes[$monitor->getSeason()]
|
$this->tmdb->tvshowDetails($monitor->getImdbId())->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());
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
|||||||
use App\Monitor\Action\Result\MonitorTvShowResult;
|
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\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use App\Base\Util\PTN;
|
use App\Base\Util\PTN;
|
||||||
@@ -29,7 +28,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private Tmdb $tmdb,
|
private TmdbClient $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
@@ -53,7 +52,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
|
|
||||||
// Compare against list from TMDB
|
// Compare against list from TMDB
|
||||||
$episodesInShow = Map::from(
|
$episodesInShow = Map::from(
|
||||||
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes
|
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
|
||||||
)->flat(1);
|
)->flat(1);
|
||||||
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Monitor\Dto;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class UpcomingEpisode
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $title,
|
|
||||||
public string $airDate {
|
|
||||||
get => Carbon::parse($this->airDate)->format('m/d/Y');
|
|
||||||
},
|
|
||||||
public string $episodeTitle,
|
|
||||||
public int $episodeNumber,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -4,30 +4,38 @@ namespace App\Monitor\Framework\Controller;
|
|||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
use Spatie\IcalendarGenerator\Components\Calendar;
|
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||||
use Spatie\IcalendarGenerator\Components\Event;
|
use Spatie\IcalendarGenerator\Components\Event;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
class CalendarController extends AbstractController
|
class CalendarController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/monitors/ical.ics', name: 'app.monitors.ical')]
|
#[IsGranted('PUBLIC_ACCESS')]
|
||||||
public function icalAction(MonitorRepository $monitorRepository)
|
#[Route('/monitors/ical/{email:user}/upcoming-episodes.ics', name: 'app.monitors.ical')]
|
||||||
|
public function icalAction(MonitorRepository $monitorRepository, User $user)
|
||||||
{
|
{
|
||||||
$calendar = new Calendar();
|
if (false === $user->hasICalEnabled()) {
|
||||||
|
return new Response('Calendar not found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$calendar = Calendar::create()
|
||||||
|
->name('Upcoming Episodes')
|
||||||
|
->refreshInterval(10);
|
||||||
|
|
||||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
$events = Map::from($monitors)->map(function ($monitor) {
|
$calendar->event(Map::from($monitors)->map(function ($monitor) {
|
||||||
return new Event($monitor->getTitle())
|
return new Event($monitor->getTitle())
|
||||||
->startsAt($monitor->getAirDate())
|
->startsAt($monitor->getAirDate())
|
||||||
->withoutTimezone()
|
->fullDay();
|
||||||
->fullDay()
|
})->toArray());
|
||||||
;
|
|
||||||
});
|
|
||||||
$calendar->event($events->toArray());
|
|
||||||
return new Response($calendar->get(), 200, [
|
return new Response($calendar->get(), 200, [
|
||||||
'Content-Type' => 'text/calendar',
|
'Content-Type' => 'text/calendar',
|
||||||
'Content-Disposition' => 'attachment; filename="upcoming-episodes.ics"',
|
'Content-Disposition' => 'inline; filename="upcoming-episodes.ics"',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Handler;
|
namespace App\Search\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
use App\Search\Action\Command\GetMediaInfoCommand;
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
use App\Search\Action\Result\GetMediaInfoResult;
|
use App\Search\Action\Result\GetMediaInfoResult;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
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;
|
||||||
@@ -13,13 +15,30 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
class GetMediaInfoHandler implements HandlerInterface
|
class GetMediaInfoHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly TmdbClient $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$handlers = [
|
||||||
|
MediaType::Movie->value => 'getMovieDetails',
|
||||||
|
MediaType::TvShow->value => 'getTvshowDetails',
|
||||||
|
];
|
||||||
|
$handler = $handlers[$command->mediaType];
|
||||||
|
$media = $this->$handler($command);
|
||||||
|
$relatedMedia = $this->tmdb->relatedMedia($media->tmdbId, $command->mediaType);
|
||||||
|
|
||||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
return new GetMediaInfoResult($media, $relatedMedia, $command->season, $command->episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMovieDetails(CommandInterface $command): TmdbResult
|
||||||
|
{
|
||||||
|
return $this->tmdb->movieDetails($command->imdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTvshowDetails(CommandInterface $command): TmdbResult
|
||||||
|
{
|
||||||
|
$media = $this->tmdb->tvshowDetails($command->imdbId);
|
||||||
|
return $media;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Handler;
|
namespace App\Search\Action\Handler;
|
||||||
|
|
||||||
use App\Base\Util\ImdbMatcher;
|
|
||||||
use App\Search\Action\Result\RedirectToMediaResult;
|
use App\Search\Action\Result\RedirectToMediaResult;
|
||||||
use App\Search\Action\Result\SearchResult;
|
use App\Search\Action\Result\SearchResult;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
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;
|
||||||
@@ -14,13 +14,13 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
class SearchHandler implements HandlerInterface
|
class SearchHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Tmdb $tmdb,
|
private TmdbClient $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
if (ImdbMatcher::isMatch($command->term)) {
|
$result = $this->tmdb->search($command->term);
|
||||||
$result = $this->tmdb->findByImdbId($command->term);
|
if ($result instanceof TmdbResult) {
|
||||||
return new RedirectToMediaResult(
|
return new RedirectToMediaResult(
|
||||||
imdbId: $result->imdbId,
|
imdbId: $result->imdbId,
|
||||||
mediaType: $result->mediaType,
|
mediaType: $result->mediaType,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Result;
|
namespace App\Search\Action\Result;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ class GetMediaInfoResult implements ResultInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $media,
|
public TmdbResult $media,
|
||||||
|
public Map|array $relatedMedia,
|
||||||
public ?int $season,
|
public ?int $season,
|
||||||
public ?int $episode,
|
public ?int $episode,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Result;
|
namespace App\Search\Action\Result;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
/** @implements ResultInterface<SearchResult> */
|
/** @implements ResultInterface<SearchResult> */
|
||||||
@@ -9,6 +10,6 @@ class SearchResult implements ResultInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $term = "",
|
public string $term = "",
|
||||||
public array $results = []
|
public Map|array $results = [],
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ use App\Search\Action\Handler\SearchHandler;
|
|||||||
use App\Search\Action\Input\GetMediaInfoInput;
|
use App\Search\Action\Input\GetMediaInfoInput;
|
||||||
use App\Search\Action\Input\SearchInput;
|
use App\Search\Action\Input\SearchInput;
|
||||||
use App\Search\Action\Result\RedirectToMediaResult;
|
use App\Search\Action\Result\RedirectToMediaResult;
|
||||||
use App\Tmdb\TmdbResult;
|
|
||||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Tmdb\Framework\Controller;
|
namespace App\Tmdb\Framework\Controller;
|
||||||
|
|
||||||
use App\Base\Util\ImdbMatcher;
|
use App\Base\Util\ImdbMatcher;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -13,7 +13,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
class ApiController extends AbstractController
|
class ApiController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/api/tmdb/ajax-search', name: 'api_tmdb_ajax_search', methods: ['GET'])]
|
#[Route('/api/tmdb/ajax-search', name: 'api_tmdb_ajax_search', methods: ['GET'])]
|
||||||
public function test(Tmdb $tmdb, Request $request): Response
|
public function test(TmdbClient $tmdb, Request $request): Response
|
||||||
{
|
{
|
||||||
$results = [];
|
$results = [];
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class ApiController extends AbstractController
|
|||||||
|
|
||||||
if (null !== $term) {
|
if (null !== $term) {
|
||||||
if (ImdbMatcher::isMatch($term)) {
|
if (ImdbMatcher::isMatch($term)) {
|
||||||
$tmdbResult = $tmdb->findByImdbId($term);
|
$tmdbResult = $tmdb->search($term);
|
||||||
$results = [
|
$results = [
|
||||||
[
|
[
|
||||||
'data' => $tmdbResult,
|
'data' => $tmdbResult,
|
||||||
|
|||||||
@@ -1,423 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Tmdb;
|
|
||||||
|
|
||||||
use Aimeos\Map;
|
|
||||||
use App\Base\Enum\MediaType;
|
|
||||||
use App\ValueObject\ResultFactory;
|
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
||||||
use Symfony\Contracts\Cache\ItemInterface;
|
|
||||||
use Tmdb\Api\Find;
|
|
||||||
use Tmdb\Client;
|
|
||||||
use Tmdb\Event\BeforeRequestEvent;
|
|
||||||
use Tmdb\Event\Listener\Psr6CachedRequestListener;
|
|
||||||
use Tmdb\Event\Listener\Request\AcceptJsonRequestListener;
|
|
||||||
use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
|
|
||||||
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
|
||||||
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
|
||||||
use Tmdb\Event\RequestEvent;
|
|
||||||
use Tmdb\Model\Movie;
|
|
||||||
use Tmdb\Model\Search\SearchQuery\KeywordSearchQuery;
|
|
||||||
use Tmdb\Model\Tv;
|
|
||||||
use Tmdb\Repository\MovieRepository;
|
|
||||||
use Tmdb\Repository\SearchRepository;
|
|
||||||
use Tmdb\Repository\TvEpisodeRepository;
|
|
||||||
use Tmdb\Repository\TvRepository;
|
|
||||||
use Tmdb\Repository\TvSeasonRepository;
|
|
||||||
use Tmdb\Token\Api\ApiToken;
|
|
||||||
use Tmdb\Token\Api\BearerToken;
|
|
||||||
|
|
||||||
class Tmdb
|
|
||||||
{
|
|
||||||
protected Client $client;
|
|
||||||
|
|
||||||
protected MovieRepository $movieRepository;
|
|
||||||
|
|
||||||
protected TvRepository $tvRepository;
|
|
||||||
|
|
||||||
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly CacheItemPoolInterface $cache,
|
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
|
||||||
) {
|
|
||||||
$this->client = new Client(
|
|
||||||
[
|
|
||||||
/** @var ApiToken|BearerToken */
|
|
||||||
'api_token' => new BearerToken($apiKey),
|
|
||||||
'secure' => true,
|
|
||||||
'base_uri' => Client::TMDB_URI,
|
|
||||||
'event_dispatcher' => [
|
|
||||||
'adapter' => $this->eventDispatcher,
|
|
||||||
],
|
|
||||||
// We make use of PSR-17 and PSR-18 auto discovery to automatically guess these, but preferably set these explicitly.
|
|
||||||
'http' => [
|
|
||||||
'client' => null,
|
|
||||||
'request_factory' => null,
|
|
||||||
'response_factory' => null,
|
|
||||||
'stream_factory' => null,
|
|
||||||
'uri_factory' => null,
|
|
||||||
],
|
|
||||||
'hydration' => [
|
|
||||||
'event_listener_handles_hydration' => false,
|
|
||||||
'only_for_specified_models' => []
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required event listeners and events to be registered with the PSR-14 Event Dispatcher.
|
|
||||||
*/
|
|
||||||
$requestListener = new Psr6CachedRequestListener(
|
|
||||||
$this->client->getHttpClient(),
|
|
||||||
$this->eventDispatcher,
|
|
||||||
$cache,
|
|
||||||
$this->client->getHttpClient()->getPsr17StreamFactory(),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
$this->eventDispatcher->addListener(RequestEvent::class, $requestListener);
|
|
||||||
|
|
||||||
$apiTokenListener = new ApiTokenRequestListener($this->client->getToken());
|
|
||||||
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $apiTokenListener);
|
|
||||||
|
|
||||||
$acceptJsonListener = new AcceptJsonRequestListener();
|
|
||||||
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $acceptJsonListener);
|
|
||||||
|
|
||||||
$jsonContentTypeListener = new ContentTypeJsonRequestListener();
|
|
||||||
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $jsonContentTypeListener);
|
|
||||||
|
|
||||||
$userAgentListener = new UserAgentRequestListener();
|
|
||||||
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $userAgentListener);
|
|
||||||
|
|
||||||
$this->movieRepository = new MovieRepository($this->client);
|
|
||||||
$this->tvRepository = new TvRepository($this->client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function popularMovies(int $page = 1, ?int $limit = null)
|
|
||||||
{
|
|
||||||
$movies = $this->movieRepository->getPopular(['page' => $page]);
|
|
||||||
|
|
||||||
$movies = $movies->map(function ($movie) use ($movies) {
|
|
||||||
return $this->parseResult($movies[$movie], "movie");
|
|
||||||
});
|
|
||||||
|
|
||||||
$movies = Map::from($movies->toArray())->filter(function ($movie) {
|
|
||||||
return $movie !== null
|
|
||||||
&& $movie->imdbId !== null
|
|
||||||
&& $movie->tmdbId !== null
|
|
||||||
&& $movie->title !== null
|
|
||||||
&& $movie->poster !== null
|
|
||||||
&& $movie->description !== null
|
|
||||||
&& $movie->mediaType !== null;
|
|
||||||
});
|
|
||||||
|
|
||||||
$movies = array_values($movies->toArray());
|
|
||||||
|
|
||||||
if (null !== $limit) {
|
|
||||||
$movies = array_slice($movies, 0, $limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $movies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function popularTvShows(int $page = 1, ?int $limit = null)
|
|
||||||
{
|
|
||||||
$movies = $this->tvRepository->getPopular(['page' => $page]);
|
|
||||||
|
|
||||||
$movies = $movies->map(function ($movie) use ($movies) {
|
|
||||||
return $this->parseResult($movies[$movie], "movie");
|
|
||||||
});
|
|
||||||
|
|
||||||
$movies = Map::from($movies->toArray())->filter(function ($movie) {
|
|
||||||
return $movie !== null
|
|
||||||
&& $movie->imdbId !== null
|
|
||||||
&& $movie->tmdbId !== null
|
|
||||||
&& $movie->title !== null
|
|
||||||
&& $movie->poster !== null
|
|
||||||
&& $movie->description !== null
|
|
||||||
&& $movie->mediaType !== null;
|
|
||||||
});
|
|
||||||
|
|
||||||
$movies = array_values($movies->toArray());
|
|
||||||
|
|
||||||
if (null !== $limit) {
|
|
||||||
$movies = array_slice($movies, 0, $limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $movies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function search(string $term, int $page = 1)
|
|
||||||
{
|
|
||||||
$searchRepository = new SearchRepository($this->client);
|
|
||||||
$searchResults = $searchRepository->searchMulti($term, new KeywordSearchQuery(['page' => $page]));
|
|
||||||
|
|
||||||
$results = [];
|
|
||||||
foreach ($searchResults as $result) {
|
|
||||||
if (!$result instanceof Movie && !$result instanceof Tv) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$results[] = $this->parseResult($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
$results = array_filter($results, fn ($result) => null !== $result->imdbId);
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find(string $id)
|
|
||||||
{
|
|
||||||
$finder = new Find($this->client);
|
|
||||||
$result = $finder->findBy($id, ['external_source' => 'imdb_id']);
|
|
||||||
|
|
||||||
if (count($result['movie_results']) > 0) {
|
|
||||||
return $result['movie_results'][0]['id'];
|
|
||||||
} elseif (count($result['tv_results']) > 0) {
|
|
||||||
return $result['tv_results'][0]['id'];
|
|
||||||
} elseif (count($result['tv_episode_results']) > 0) {
|
|
||||||
return $result['tv_episode_results'][0]['show_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \Exception("No results found for $id");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findByImdbId(string $imdbId)
|
|
||||||
{
|
|
||||||
$finder = new Find($this->client);
|
|
||||||
$result = $finder->findBy($imdbId, ['external_source' => 'imdb_id']);
|
|
||||||
|
|
||||||
if (count($result['movie_results']) > 0) {
|
|
||||||
$result = $result['movie_results'][0];
|
|
||||||
$mediaType = MediaType::Movie->value;
|
|
||||||
} elseif (count($result['tv_results']) > 0) {
|
|
||||||
$result = $result['tv_results'][0];
|
|
||||||
$mediaType = MediaType::TvShow->value;
|
|
||||||
} elseif (count($result['tv_episode_results']) > 0) {
|
|
||||||
$result = $result['tv_episode_results'][0];
|
|
||||||
$mediaType = MediaType::TvShow->value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result['media_type'] = $mediaType;
|
|
||||||
$result = $this->mediaDetails($imdbId, $result['media_type']);
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function movieDetails(string $id)
|
|
||||||
{
|
|
||||||
$client = new MovieRepository($this->client);
|
|
||||||
$details = $client->getApi()->getMovie($id, ['append_to_response' => 'external_ids']);
|
|
||||||
return $this->parseResult($details, "movie");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function tvDetails(string $id)
|
|
||||||
{
|
|
||||||
$client = new TvRepository($this->client);
|
|
||||||
$details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
|
|
||||||
$details = $this->getEpisodesFromSeries($details);
|
|
||||||
return $this->parseResult($details, "tvshow");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function episodeDetails(string $id, string $season, string $episode)
|
|
||||||
{
|
|
||||||
$client = new TvEpisodeRepository($this->client);
|
|
||||||
$result = $client->getApi()->getEpisode($id, $season, $episode, ['append_to_response' => 'external_ids']);
|
|
||||||
return $this->parseResult($result, "episode");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEpisodesFromSeries(array $series)
|
|
||||||
{
|
|
||||||
$client = new TvSeasonRepository($this->client);
|
|
||||||
foreach ($series['seasons'] as $season) {
|
|
||||||
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$series['episodes'][$season['season_number']] = Map::from(
|
|
||||||
$client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
|
|
||||||
)->map(function ($data) {
|
|
||||||
$data['poster'] = (null !== $data['still_path']) ? self::POSTER_IMG_PATH . $data['still_path'] : null;
|
|
||||||
return $data;
|
|
||||||
})->toArray();
|
|
||||||
}
|
|
||||||
return $series;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mediaDetails(string $id, string $type)
|
|
||||||
{
|
|
||||||
$id = $this->find($id);
|
|
||||||
|
|
||||||
if ($type === "movies") {
|
|
||||||
return $this->movieDetails($id);
|
|
||||||
} else {
|
|
||||||
return $this->tvDetails($id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseResult($result, $mediaType = null)
|
|
||||||
{
|
|
||||||
if (is_array($result)) {
|
|
||||||
return $this->parseFromArray($result, $mediaType);
|
|
||||||
} else {
|
|
||||||
return $this->parseFromObject($result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseFromArray($data, $mediaType)
|
|
||||||
{
|
|
||||||
if (null === $mediaType) {
|
|
||||||
throw new \Exception("A media type must be set when parsing from an array.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mediaType === 'movie') {
|
|
||||||
$result = $this->parseMovie($data, self::POSTER_IMG_PATH);
|
|
||||||
} elseif ($mediaType === 'tvshow') {
|
|
||||||
$result = $this->parseTvShow($data, self::POSTER_IMG_PATH);
|
|
||||||
} elseif ($mediaType === 'episode') {
|
|
||||||
$result = $this->parseEpisode($data, self::POSTER_IMG_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseTvShow(array $data, string $posterBasePath): TmdbResult
|
|
||||||
{
|
|
||||||
if (!in_array($data['first_air_date'], ['', null,])) {
|
|
||||||
$airDate = (new \DateTime($data['first_air_date']))->format('Y-m-d');
|
|
||||||
} else {
|
|
||||||
$airDate = null;
|
|
||||||
}
|
|
||||||
return new TmdbResult(
|
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
|
||||||
tmdbId: $data['id'],
|
|
||||||
title: $data['name'],
|
|
||||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
|
||||||
description: $data['overview'],
|
|
||||||
year: $airDate,
|
|
||||||
mediaType: "tvshows",
|
|
||||||
episodes: $data['episodes'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseEpisode(array $data, string $posterBasePath): TmdbResult
|
|
||||||
{
|
|
||||||
if (!in_array($data['air_date'], ['', null,])) {
|
|
||||||
$airDate = (new \DateTime($data['air_date']))->format('Y-m-d');
|
|
||||||
} else {
|
|
||||||
$airDate = null;
|
|
||||||
}
|
|
||||||
return new TmdbResult(
|
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
|
||||||
tmdbId: $data['id'],
|
|
||||||
title: $data['name'],
|
|
||||||
poster: (null !== $data['still_path']) ? $posterBasePath . $data['still_path'] : null,
|
|
||||||
description: $data['overview'],
|
|
||||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
|
||||||
mediaType: "tvshows",
|
|
||||||
episodes: null,
|
|
||||||
episodeAirDate: $airDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseMovie(array $data, string $posterBasePath): TmdbResult
|
|
||||||
{
|
|
||||||
if (!in_array($data['release_date'], ['', null,])) {
|
|
||||||
$airDate = (new \DateTime($data['release_date']))->format('Y-m-d');
|
|
||||||
} else {
|
|
||||||
$airDate = null;
|
|
||||||
}
|
|
||||||
return new TmdbResult(
|
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
|
||||||
tmdbId: $data['id'],
|
|
||||||
title: $data['title'],
|
|
||||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
|
||||||
description: $data['overview'],
|
|
||||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
|
||||||
mediaType: "movies",
|
|
||||||
episodeAirDate: $airDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private function parseFromObject($result): TmdbResult
|
|
||||||
{
|
|
||||||
$mediaType = $result instanceof Movie ? MediaType::Movie->value : MediaType::TvShow->value;
|
|
||||||
$tmdbResult = new TmdbResult();
|
|
||||||
$tmdbResult->mediaType = $mediaType;
|
|
||||||
$tmdbResult->tmdbId = $result->getId();
|
|
||||||
$tmdbResult->imdbId = $this->getImdbId($result->getId(), $mediaType);
|
|
||||||
$tmdbResult->title = $this->getTitle($result, $mediaType);
|
|
||||||
$tmdbResult->poster = self::POSTER_IMG_PATH . $result->getPosterImage();
|
|
||||||
$tmdbResult->year = $this->getReleaseDate($result, $mediaType);
|
|
||||||
$tmdbResult->description = $result->getOverview();
|
|
||||||
return $tmdbResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getImdbId(string $tmdbId, $mediaType)
|
|
||||||
{
|
|
||||||
$externalIds = $this->cache->get("tmdb.externalIds.{$tmdbId}",
|
|
||||||
function (ItemInterface $item) use ($tmdbId, $mediaType) {
|
|
||||||
switch (MediaType::tryFrom($mediaType)->value) {
|
|
||||||
case MediaType::Movie->value:
|
|
||||||
return $this->movieRepository->getExternalIds($tmdbId);
|
|
||||||
case MediaType::TvShow->value:
|
|
||||||
return $this->tvRepository->getExternalIds($tmdbId);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (null === $externalIds) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $externalIds->getImdbId() !== "" ? $externalIds->getImdbId() : "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getImages($tmdbId, $mediaType)
|
|
||||||
{
|
|
||||||
return $this->cache->get("tmdb.images.{$tmdbId}",
|
|
||||||
function (ItemInterface $item) use ($tmdbId, $mediaType) {
|
|
||||||
switch (MediaType::tryFrom($mediaType)->value) {
|
|
||||||
case MediaType::Movie->value:
|
|
||||||
return $this->movieRepository->getImages($tmdbId);
|
|
||||||
case MediaType::TvShow->value:
|
|
||||||
return $this->tvRepository->getImages($tmdbId);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getReleaseDate($result, $mediaType): string
|
|
||||||
{
|
|
||||||
switch (MediaType::tryFrom($mediaType)->value) {
|
|
||||||
case MediaType::Movie->value:
|
|
||||||
return ($result->getReleaseDate() instanceof \DateTime)
|
|
||||||
? $result->getReleaseDate()->format('Y')
|
|
||||||
: $result->getReleaseDate();
|
|
||||||
case MediaType::TvShow->value:
|
|
||||||
return ($result->getFirstAirDate() instanceof \DateTime)
|
|
||||||
? $result->getFirstAirDate()->format('Y')
|
|
||||||
: $result->getFirstAirDate();
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTitle($result, $mediaType): string
|
|
||||||
{
|
|
||||||
switch (MediaType::tryFrom($mediaType)->value) {
|
|
||||||
case MediaType::Movie->value:
|
|
||||||
return $result->getTitle();
|
|
||||||
case MediaType::TvShow->value:
|
|
||||||
return $result->getName();
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
269
src/Tmdb/TmdbClient.php
Normal file
269
src/Tmdb/TmdbClient.php
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Base\Util\ImdbMatcher;
|
||||||
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
use Tmdb\Api\Find;
|
||||||
|
use Tmdb\Client;
|
||||||
|
use Tmdb\Event\BeforeRequestEvent;
|
||||||
|
use Tmdb\Event\Listener\Psr6CachedRequestListener;
|
||||||
|
use Tmdb\Event\Listener\Request\AcceptJsonRequestListener;
|
||||||
|
use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
|
||||||
|
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
||||||
|
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
||||||
|
use Tmdb\Event\RequestEvent;
|
||||||
|
use Tmdb\Repository\MovieRepository;
|
||||||
|
use Tmdb\Repository\SearchRepository;
|
||||||
|
use Tmdb\Repository\TvEpisodeRepository;
|
||||||
|
use Tmdb\Repository\TvRepository;
|
||||||
|
use Tmdb\Repository\TvSeasonRepository;
|
||||||
|
use Tmdb\Token\Api\ApiToken;
|
||||||
|
use Tmdb\Token\Api\BearerToken;
|
||||||
|
|
||||||
|
class TmdbClient
|
||||||
|
{
|
||||||
|
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
||||||
|
|
||||||
|
protected Client $client;
|
||||||
|
protected MovieRepository $movieRepository;
|
||||||
|
protected TvRepository $tvRepository;
|
||||||
|
protected TvSeasonRepository $tvSeasonRepository;
|
||||||
|
protected TvEpisodeRepository $tvEpisodeRepository;
|
||||||
|
protected SearchRepository $searchRepository;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly CacheItemPoolInterface $cache,
|
||||||
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
|
) {
|
||||||
|
$this->client = new Client(
|
||||||
|
[
|
||||||
|
/** @var ApiToken|BearerToken */
|
||||||
|
'api_token' => new BearerToken($apiKey),
|
||||||
|
'secure' => true,
|
||||||
|
'base_uri' => Client::TMDB_URI,
|
||||||
|
'event_dispatcher' => [
|
||||||
|
'adapter' => $this->eventDispatcher,
|
||||||
|
],
|
||||||
|
// We make use of PSR-17 and PSR-18 auto discovery to automatically guess these, but preferably set these explicitly.
|
||||||
|
'http' => [
|
||||||
|
'client' => null,
|
||||||
|
'request_factory' => null,
|
||||||
|
'response_factory' => null,
|
||||||
|
'stream_factory' => null,
|
||||||
|
'uri_factory' => null,
|
||||||
|
],
|
||||||
|
'hydration' => [
|
||||||
|
'event_listener_handles_hydration' => false,
|
||||||
|
'only_for_specified_models' => []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required event listeners and events to be registered with the PSR-14 Event Dispatcher.
|
||||||
|
*/
|
||||||
|
$requestListener = new Psr6CachedRequestListener(
|
||||||
|
$this->client->getHttpClient(),
|
||||||
|
$this->eventDispatcher,
|
||||||
|
$cache,
|
||||||
|
$this->client->getHttpClient()->getPsr17StreamFactory(),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
$this->eventDispatcher->addListener(RequestEvent::class, $requestListener);
|
||||||
|
|
||||||
|
$apiTokenListener = new ApiTokenRequestListener($this->client->getToken());
|
||||||
|
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $apiTokenListener);
|
||||||
|
|
||||||
|
$acceptJsonListener = new AcceptJsonRequestListener();
|
||||||
|
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $acceptJsonListener);
|
||||||
|
|
||||||
|
$jsonContentTypeListener = new ContentTypeJsonRequestListener();
|
||||||
|
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $jsonContentTypeListener);
|
||||||
|
|
||||||
|
$userAgentListener = new UserAgentRequestListener();
|
||||||
|
$this->eventDispatcher->addListener(BeforeRequestEvent::class, $userAgentListener);
|
||||||
|
|
||||||
|
$this->movieRepository = new MovieRepository($this->client);
|
||||||
|
$this->tvRepository = new TvRepository($this->client);
|
||||||
|
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
|
||||||
|
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
|
||||||
|
$this->searchRepository = new SearchRepository($this->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(string $term): TmdbResult|Map
|
||||||
|
{
|
||||||
|
if (ImdbMatcher::isMatch($term)) {
|
||||||
|
$handlers = [
|
||||||
|
'movie' => 'movieDetails',
|
||||||
|
'tvshow' => 'tvshowDetails',
|
||||||
|
];
|
||||||
|
$data = $this->findByImdbId($term);
|
||||||
|
$handler = $handlers[$data['media_type']];
|
||||||
|
return $this->$handler($term);
|
||||||
|
}
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$this->searchRepository->getApi()->searchMulti($term),
|
||||||
|
"movies"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function movieDetails(string $imdbId): ?TmdbResult
|
||||||
|
{
|
||||||
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
|
return $this->parseResult(
|
||||||
|
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids']),
|
||||||
|
MediaType::Movie->value,
|
||||||
|
$imdbId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tvshowDetails(string $imdbId): ?TmdbResult
|
||||||
|
{
|
||||||
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
|
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids']);
|
||||||
|
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
|
||||||
|
return $data['season_number'] !== 0 &&
|
||||||
|
strtolower($data['name']) !== 'specials';
|
||||||
|
})->map(function ($data) use ($media) {
|
||||||
|
return $this->tvSeasonDetails($media['id'], $data['season_number'])['episodes'];
|
||||||
|
})->rekey(function ($data) {
|
||||||
|
return $data[1]['season_number'];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
return $this->parseResult(
|
||||||
|
$media,
|
||||||
|
MediaType::TvShow->value,
|
||||||
|
$imdbId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tvSeasonDetails(string $tmdbId, int $season): array
|
||||||
|
{
|
||||||
|
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season);
|
||||||
|
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
|
||||||
|
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
|
||||||
|
$data['poster'] = $data['still_path'];
|
||||||
|
return $data;
|
||||||
|
})->rekey(fn ($data) => $data['episode_number'])->toArray();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): ?TmdbResult
|
||||||
|
{
|
||||||
|
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids']);
|
||||||
|
return $this->parseResult(
|
||||||
|
$result,
|
||||||
|
MediaType::TvEpisode->value,
|
||||||
|
$result['external_ids']['imdb_id']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relatedMedia(string $tmdbId, string $mediaType, int $resultCount = 6): Map
|
||||||
|
{
|
||||||
|
$repos = [
|
||||||
|
'movies' => $this->movieRepository,
|
||||||
|
'tvshows' => $this->tvRepository,
|
||||||
|
];
|
||||||
|
$results = $repos[$mediaType]->getApi()->getRecommendations($tmdbId);
|
||||||
|
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$results,
|
||||||
|
$mediaType,
|
||||||
|
$resultCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function popularMovies(int $resultCount = 6): Map
|
||||||
|
{
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$this->movieRepository->getApi()->getPopular(),
|
||||||
|
"movies",
|
||||||
|
$resultCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function popularTvShows(int $resultCount = 6): Map
|
||||||
|
{
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$this->tvRepository->getApi()->getPopular(),
|
||||||
|
"tvshows",
|
||||||
|
$resultCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getExternalIds(string $tmdbId, $mediaType): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
switch (MediaType::tryFrom($mediaType)->value) {
|
||||||
|
case MediaType::Movie->value:
|
||||||
|
$externalIds = $this->movieRepository->getApi()->getExternalIds($tmdbId);
|
||||||
|
break;
|
||||||
|
case MediaType::TvShow->value:
|
||||||
|
$externalIds = $this->tvRepository->getApi()->getExternalIds($tmdbId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$externalIds = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (\throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $externalIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findByImdbId(string $imdbId): array
|
||||||
|
{
|
||||||
|
$finder = new Find($this->client);
|
||||||
|
$result = $finder->findBy($imdbId, ['external_source' => 'imdb_id']);
|
||||||
|
|
||||||
|
if (count($result['movie_results']) > 0) {
|
||||||
|
return $result['movie_results'][0];
|
||||||
|
} elseif (count($result['tv_results']) > 0) {
|
||||||
|
return $result['tv_results'][0];
|
||||||
|
} elseif (count($result['tv_episode_results']) > 0) {
|
||||||
|
return $result['tv_episode_results'][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception("No results found for $imdbId");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseResult(array $data, string $mediaType, string $imdbId): TmdbResult
|
||||||
|
{
|
||||||
|
if (!array_key_exists('external_ids', $data)) {
|
||||||
|
$data['external_ids'] = ['imdb_id' => $imdbId];
|
||||||
|
}
|
||||||
|
return $this->serializer->denormalize($data, TmdbResult::class, context: ['media_type' => $mediaType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseListOfResults(array $data, string $mediaType, ?int $resultCount = null): Map
|
||||||
|
{
|
||||||
|
$results = Map::from($data['results'])->filter(
|
||||||
|
fn ($result) => array_key_exists('id', $result)
|
||||||
|
)->map(function ($result) {
|
||||||
|
$result['external_ids'] = $this->getExternalIds($result['id'], MediaType::Movie->value);
|
||||||
|
return $result;
|
||||||
|
})->filter(function ($result) {
|
||||||
|
return array_key_exists('id', $result) &&
|
||||||
|
array_key_exists('imdb_id', $result['external_ids']) &&
|
||||||
|
$result['external_ids']['imdb_id'] !== null &&
|
||||||
|
$result['external_ids']['imdb_id'] !== "";
|
||||||
|
})->map(function ($result) use ($mediaType) {
|
||||||
|
return $this->serializer->denormalize($result, TmdbResult::class, context: ['media_type' => $mediaType]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (null !== $resultCount) {
|
||||||
|
$results = $results->slice(0, $resultCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,23 @@
|
|||||||
|
|
||||||
namespace App\Tmdb;
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
class TmdbResult
|
class TmdbResult
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[SerializedPath('[external_ids][imdb_id]')]
|
||||||
public ?string $imdbId = "",
|
public ?string $imdbId = "",
|
||||||
public ?string $tmdbId = "",
|
#[SerializedPath('[id]')]
|
||||||
|
public ?int $tmdbId = null,
|
||||||
public ?string $title = "",
|
public ?string $title = "",
|
||||||
public ?string $poster = "",
|
#[SerializedPath('[overview]')]
|
||||||
public ?string $description = "",
|
public ?string $description = "",
|
||||||
public ?string $year = "",
|
public ?string $poster = "",
|
||||||
|
public ?\DateTimeInterface $premiereDate = null,
|
||||||
|
public ?string $year = null,
|
||||||
public ?string $mediaType = "",
|
public ?string $mediaType = "",
|
||||||
|
#[SerializedPath('[seasons]')]
|
||||||
public ?array $episodes = null,
|
public ?array $episodes = null,
|
||||||
public ?string $episodeAirDate = null,
|
public ?string $episodeAirDate = null,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
111
src/Tmdb/TmdbResultDenormalizer.php
Normal file
111
src/Tmdb/TmdbResultDenormalizer.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
|
|
||||||
|
class TmdbResultDenormalizer implements DenormalizerInterface
|
||||||
|
{
|
||||||
|
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(service: 'serializer.normalizer.object')]
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): TmdbResult|array|null
|
||||||
|
{
|
||||||
|
$parsers = [
|
||||||
|
MediaType::Movie->value => 'parseMovie',
|
||||||
|
MediaType::TvShow->value => 'parseTvShow',
|
||||||
|
'movie' => 'parseMovie',
|
||||||
|
'tv' => 'parseTvShow',
|
||||||
|
'tvepisode' => 'parseEpisode',
|
||||||
|
];
|
||||||
|
|
||||||
|
$denormalized = $this->normalizer->denormalize($data, TmdbResult::class, $format, $context);
|
||||||
|
$parser = array_key_exists('media_type', $data) ? $parsers[$data['media_type']] : $parsers[$context['media_type']];
|
||||||
|
return $this->$parser($data, $denormalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseTvShow(array $data, TmdbResult $result): TmdbResult
|
||||||
|
{
|
||||||
|
if (!in_array($data['first_air_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['first_air_date']));
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->title = $data['original_name'];
|
||||||
|
$result->premiereDate = $airDate;
|
||||||
|
$result->poster = (null !== $data['poster_path']) ? self::POSTER_IMG_PATH . $data['poster_path'] : null;
|
||||||
|
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
||||||
|
$result->mediaType = "tvshows";
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseMovie(array $data, TmdbResult $result): TmdbResult
|
||||||
|
{
|
||||||
|
if (!in_array($data['release_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['release_date']));
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->title = $data['original_title'];
|
||||||
|
$result->premiereDate = $airDate;
|
||||||
|
$result->poster = (null !== $data['poster_path']) ? self::POSTER_IMG_PATH . $data['poster_path'] : null;
|
||||||
|
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
||||||
|
$result->mediaType = "movies";
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseEpisode(array $data, TmdbResult $result): TmdbResult
|
||||||
|
{
|
||||||
|
if (!in_array($data['air_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['air_date']));
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->premiereDate = $airDate;
|
||||||
|
$result->episodeAirDate = $airDate->format('m/d/Y');
|
||||||
|
$result->poster = (null !== $data['still_path']) ? self::POSTER_IMG_PATH . $data['still_path'] : null;
|
||||||
|
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
||||||
|
$result->mediaType = "tvshows";
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function supportsDenormalization(
|
||||||
|
mixed $data,
|
||||||
|
string $type,
|
||||||
|
?string $format = null,
|
||||||
|
array $context = []
|
||||||
|
): bool {
|
||||||
|
return $type === TmdbResult::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TmdbResult::class => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Torrentio\Action\Handler;
|
namespace App\Torrentio\Action\Handler;
|
||||||
|
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
||||||
use App\Torrentio\Client\Torrentio;
|
use App\Torrentio\Client\Torrentio;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
@@ -13,14 +13,14 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
class GetMovieOptionsHandler implements HandlerInterface
|
class GetMovieOptionsHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly TmdbClient $tmdb,
|
||||||
private readonly Torrentio $torrentio,
|
private readonly Torrentio $torrentio,
|
||||||
private readonly MediaFiles $mediaFiles
|
private readonly MediaFiles $mediaFiles
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, 'movies');
|
$media = $this->tmdb->movieDetails($command->imdbId);
|
||||||
return new GetMovieOptionsResult(
|
return new GetMovieOptionsResult(
|
||||||
media: $media,
|
media: $media,
|
||||||
file: $this->mediaFiles->movieExists($media->title),
|
file: $this->mediaFiles->movieExists($media->title),
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Action\Handler;
|
namespace App\Torrentio\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Library\Dto\MediaFileDto;
|
use App\Library\Dto\MediaFileDto;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
use App\Torrentio\Client\Torrentio;
|
use App\Torrentio\Client\Torrentio;
|
||||||
@@ -16,15 +17,15 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
class GetTvShowOptionsHandler implements HandlerInterface
|
class GetTvShowOptionsHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly TmdbClient $tmdb,
|
||||||
private readonly Torrentio $torrentio,
|
private readonly Torrentio $torrentio,
|
||||||
private readonly MediaFiles $mediaFiles,
|
private readonly MediaFiles $mediaFiles,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode);
|
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->season, $command->episode);
|
||||||
$parentShow = $this->tmdb->mediaDetails($command->imdbId, 'tvshows');
|
$parentShow = $this->tmdb->tvshowDetails($command->imdbId);
|
||||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||||
|
|
||||||
return new GetTvShowOptionsResult(
|
return new GetTvShowOptionsResult(
|
||||||
|
|||||||
55
src/Torrentio/Client/HttpClient.php
Normal file
55
src/Torrentio/Client/HttpClient.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Torrentio\Client;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use GuzzleHttp\Client as GuzzleClient;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
|
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||||
|
|
||||||
|
class HttpClient
|
||||||
|
{
|
||||||
|
private GuzzleClient $client;
|
||||||
|
|
||||||
|
private string $baseUrl = 'https://torrentio.strem.fun/realdebrid=%s/';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
|
||||||
|
private TagAwareCacheInterface $cache,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
$this->client = new GuzzleClient([
|
||||||
|
'base_uri' => sprintf($this->baseUrl, $this->realDebridKey),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $imdbId, array $cacheTags = []): array
|
||||||
|
{
|
||||||
|
$cacheKey = str_replace(":", ".", "torrentio.{$imdbId}");
|
||||||
|
|
||||||
|
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $cacheTags) {
|
||||||
|
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||||
|
if (count($cacheTags) > 0) {
|
||||||
|
$item->tag($cacheTags);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$response = $this->client->get("stream/movie/$imdbId.json");
|
||||||
|
return json_decode(
|
||||||
|
$response->getBody()->getContents(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
dd($exception);
|
||||||
|
if ($exception->getCode() === 429) {
|
||||||
|
$this->logger->warning("> [TorrentioClient] Rate limit exceeded");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents());
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,59 +2,19 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Client;
|
namespace App\Torrentio\Client;
|
||||||
|
|
||||||
use App\Torrentio\Client\Rule\DownloadOptionFilter\Resolution;
|
|
||||||
use App\Torrentio\Client\Rule\RuleEngine;
|
|
||||||
use App\Torrentio\Result\ResultFactory;
|
use App\Torrentio\Result\ResultFactory;
|
||||||
use Carbon\Carbon;
|
|
||||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
||||||
use Symfony\Contracts\Cache\ItemInterface;
|
|
||||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
|
||||||
|
|
||||||
class Torrentio
|
class Torrentio
|
||||||
{
|
{
|
||||||
private string $baseUrl = 'https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={realDebridKey}/stream/movie';
|
|
||||||
|
|
||||||
private string $searchUrl;
|
|
||||||
|
|
||||||
private Client $client;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
|
private readonly HttpClient $client,
|
||||||
private TagAwareCacheInterface $cache,
|
) {}
|
||||||
private LoggerInterface $logger,
|
|
||||||
) {
|
|
||||||
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
|
|
||||||
$this->client = new Client([
|
|
||||||
'base_uri' => $this->searchUrl,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function search(string $imdbCode, string $type, bool $parseResults = true): array
|
public function search(string $imdbCode, string $type, bool $parseResults = true): array
|
||||||
{
|
{
|
||||||
$cacheKey = "torrentio.{$imdbCode}";
|
$cacheTags = ['torrentio', $type, $imdbCode];
|
||||||
|
$results = $this->client->get($imdbCode, $cacheTags);
|
||||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
|
|
||||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
|
||||||
$item->tag(['torrentio', $type, $imdbCode]);
|
|
||||||
try {
|
|
||||||
$response = $this->client->get("$this->searchUrl/$imdbCode.json");
|
|
||||||
return json_decode(
|
|
||||||
$response->getBody()->getContents(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
} catch (\Throwable $exception) {
|
|
||||||
if ($exception->getCode() === 429) {
|
|
||||||
$this->logger->warning("> [TorrentioClient] Rate limit exceeded");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents());
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (true === $parseResults) {
|
if (true === $parseResults) {
|
||||||
return $this->parse($results);
|
return $this->parse($results);
|
||||||
@@ -65,26 +25,8 @@ class Torrentio
|
|||||||
|
|
||||||
public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): array
|
public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): array
|
||||||
{
|
{
|
||||||
$cacheKey = "torrentio.$imdbId.$season.$episode";
|
$cacheTags = ['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"];
|
||||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
|
$results = $this->client->get("$imdbId:$season:$episode", $cacheTags);
|
||||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
|
||||||
$item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
|
|
||||||
try {
|
|
||||||
$response = $this->client->get("$this->searchUrl/$imdbId:$season:$episode.json");
|
|
||||||
return json_decode(
|
|
||||||
$response->getBody()->getContents(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
} catch (\Throwable $exception) {
|
|
||||||
if ($exception->getCode() === 429) {
|
|
||||||
$this->logger->warning("> [TorrentioClient] Rate limit exceeded");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents());
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (null === $results) {
|
if (null === $results) {
|
||||||
throw new TorrentioRateLimitException();
|
throw new TorrentioRateLimitException();
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ final class TvEpisodeList
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->reloadCount++;
|
$this->reloadCount++;
|
||||||
|
// dd(new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage));
|
||||||
return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage);
|
return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Twig\Components;
|
|
||||||
|
|
||||||
use Aimeos\Map;
|
|
||||||
use App\Monitor\Dto\UpcomingEpisode;
|
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
|
||||||
use App\Tmdb\Tmdb;
|
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
|
||||||
use Tmdb\Model\Tv\Episode;
|
|
||||||
|
|
||||||
#[AsTwigComponent]
|
|
||||||
final class UpcomingEpisodes extends AbstractController
|
|
||||||
{
|
|
||||||
// Get active monitors
|
|
||||||
// Search TMDB for upcoming episodes
|
|
||||||
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly Tmdb $tmdb,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function getUpcomingEpisodes(int $limit = 5): array
|
|
||||||
{
|
|
||||||
$upcomingEpisodes = new Map();
|
|
||||||
$monitors = $this->getMonitors();
|
|
||||||
foreach ($monitors as $monitor) {
|
|
||||||
$upcomingEpisodes->merge($this->getNextEpisodes($monitor));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $upcomingEpisodes->slice(0, $limit)->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getMonitors()
|
|
||||||
{
|
|
||||||
$user = $this->getUser();
|
|
||||||
return $user->getMonitors()->filter(
|
|
||||||
fn (Monitor $monitor) => null === $monitor->getParent() && $monitor->isActive()
|
|
||||||
) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getNextEpisodes(Monitor $monitor): Map
|
|
||||||
{
|
|
||||||
$today = CarbonImmutable::now();
|
|
||||||
$seriesInfo = $this->tmdb->tvDetails($monitor->getTmdbId());
|
|
||||||
|
|
||||||
switch ($monitor->getMonitorType()) {
|
|
||||||
case "tvseason":
|
|
||||||
$episodes = Map::from($seriesInfo->episodes[$monitor->getSeason()])
|
|
||||||
->filter(function (array $episode) use ($today) {
|
|
||||||
$airDate = CarbonImmutable::parse($episode['air_date']);
|
|
||||||
return $airDate->lte($today);
|
|
||||||
})
|
|
||||||
;
|
|
||||||
break;
|
|
||||||
case "tvshows":
|
|
||||||
$episodes = [];
|
|
||||||
foreach ($seriesInfo->episodes as $season => $episodeList) {
|
|
||||||
$episodes = array_merge($episodes, $episodeList);
|
|
||||||
}
|
|
||||||
$episodes = Map::from($episodes)
|
|
||||||
->filter(function (array $episode) use ($today) {
|
|
||||||
$airDate = CarbonImmutable::parse($episode['air_date']);
|
|
||||||
return $airDate->gte($today);
|
|
||||||
})
|
|
||||||
;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $episodes->map(function (array $episode) use ($monitor) {
|
|
||||||
return new UpcomingEpisode(
|
|
||||||
$monitor->getTitle(),
|
|
||||||
$episode['air_date'],
|
|
||||||
$episode['name'],
|
|
||||||
$episode['episode_number'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $enable_ical_up_ep,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Handler;
|
||||||
|
|
||||||
|
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||||
|
use App\User\Action\Result\SaveUserDownloadPreferencesResult;
|
||||||
|
use App\User\Action\Result\SaveUserMediaPreferencesResult;
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
|
use App\User\Framework\Entity\UserPreference;
|
||||||
|
use App\User\Framework\Repository\PreferencesRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
/** @implements HandlerInterface<SaveUserMediaPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly PreferencesRepository $preferenceRepository,
|
||||||
|
private readonly Security $token,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(C $command): R
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->token->getUser();
|
||||||
|
|
||||||
|
foreach ($command as $preference => $value) {
|
||||||
|
if ($user->hasUserPreference($preference)) {
|
||||||
|
$user->updateUserPreference($preference, $value);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$preference = $this->preferenceRepository->find($preference);
|
||||||
|
|
||||||
|
$user->addUserPreference(
|
||||||
|
(new UserPreference())
|
||||||
|
->setUser($user)
|
||||||
|
->setPreference($preference)
|
||||||
|
->setPreferenceValue($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new SaveUserDownloadPreferencesResult($user->getDownloadPreferences());
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Input;
|
||||||
|
|
||||||
|
use App\User\Action\Command\SaveUserCalendarPreferencesCommand;
|
||||||
|
use App\User\Action\Command\SaveUserDownloadPreferencesCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/** @implements InputInterface<SaveUserDownloadPreferencesInput, SaveUserDownloadPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceSecurity]
|
||||||
|
public mixed $userId,
|
||||||
|
|
||||||
|
#[SourceRequest('enable_ical_up_ep', nullify: true)]
|
||||||
|
public bool $enableIcalUpcomingEpisodes,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): C
|
||||||
|
{
|
||||||
|
return new SaveUserCalendarPreferencesCommand(
|
||||||
|
$this->enableIcalUpcomingEpisodes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ namespace App\User\Framework\Controller\Web;
|
|||||||
|
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||||
|
use App\User\Action\Handler\SaveUserCalendarPreferencesHandler;
|
||||||
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
||||||
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||||
|
use App\User\Action\Input\SaveUserCalendarPreferencesInput;
|
||||||
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
||||||
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
||||||
use App\User\Database\CountryLanguages;
|
use App\User\Database\CountryLanguages;
|
||||||
@@ -33,15 +35,15 @@ class PreferencesController extends AbstractController
|
|||||||
public function mediaPreferences(): Response
|
public function mediaPreferences(): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
// dd($form);
|
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -54,8 +56,8 @@ class PreferencesController extends AbstractController
|
|||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class);
|
$form = $this->createForm(UserMediaPreferencesForm::class);
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
@@ -69,6 +71,7 @@ class PreferencesController extends AbstractController
|
|||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -81,6 +84,7 @@ class PreferencesController extends AbstractController
|
|||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
@@ -95,6 +99,34 @@ class PreferencesController extends AbstractController
|
|||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
|
'preferences_form' => $form,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/user/preferences/calendar', 'app.save.calendar-preferences', methods: ['POST'])]
|
||||||
|
public function saveCalendarPreferences(
|
||||||
|
SaveUserCalendarPreferencesInput $input,
|
||||||
|
SaveUserCalendarPreferencesHandler $handler,
|
||||||
|
): Response
|
||||||
|
{
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
|
$handler->handle($input->toCommand());
|
||||||
|
|
||||||
|
$this->broadcaster->alert(
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Your calendar preferences have been saved.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render(
|
||||||
|
'user/preferences.html.twig',
|
||||||
|
[
|
||||||
|
'downloadPreferences' => $this->getUser()->getDownloadPreferences(),
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -327,4 +327,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCalendarPreferences(): array
|
||||||
|
{
|
||||||
|
return Map::from($this->userPreferences)
|
||||||
|
->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId())
|
||||||
|
->filter(fn(UserPreference $userPreference) => $userPreference->getPreference()->getType() === 'calendar')
|
||||||
|
->toArray()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasICalEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->hasUserPreference('enable_ical_up_ep') &&
|
||||||
|
(bool) $this->getUserPreference('enable_ical_up_ep')->getPreferenceValue() === true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<twig:SearchBar />
|
<twig:SearchBar />
|
||||||
<div class="md:flex md:items-center md:gap-12">
|
<div class="md:flex md:items-center md:gap-12">
|
||||||
<nav aria-label="Global" class="md:block">
|
<nav aria-label="Global" class="md:block">
|
||||||
<ul class="flex items-center gap-6 text-sm">
|
<ul class="ml-4 flex items-end md:items-center md:gap-6 text-sm">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
||||||
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||||
<tr id="monitor_view_all">
|
<tr id="monitor_view_all">
|
||||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-gray-400 dark:bg-gray-700 uppercase text-xs font-medium text-center text-black dark:text-white min-w-[50ch] max-w-[50ch] truncate">
|
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
||||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block h2 %}Upcoming Episodes{% endblock %}
|
{% block h2 %}Upcoming Episodes{% endblock %}
|
||||||
|
|
||||||
{% block action_buttons %}
|
{% block action_buttons %}
|
||||||
<a href="{{ path('app.monitor.upcoming-episodes') }}"
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false"
|
||||||
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||||
Upcoming Episodes
|
Upcoming Episodes
|
||||||
</a>
|
</a>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<twig:Card title="Upcoming episodes of shows your monitoring">
|
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||||
<a href="{{ path('app.monitors.ical') }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
||||||
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||||
</a>
|
</a>
|
||||||
<div id="calendar" class="text-white">
|
<div id="calendar" class="text-white">
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
||||||
|
|
||||||
|
{% block h2 %}Media Results{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col grow gap-4">
|
<div class="p-4 flex flex-col grow gap-4">
|
||||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
|
||||||
<div class="flex flex-row w-full gap-2">
|
<div class="flex flex-row w-full gap-2">
|
||||||
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
||||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="mb-4 flex flex-row gap-2 justify-between">
|
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||||
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
{{ results.media.title }} ({{ results.media.year }})
|
{{ results.media.title }} ({{ results.media.year|date('Y') }})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
@@ -95,12 +96,35 @@
|
|||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
results="results"
|
results="results"
|
||||||
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
loading="defer"
|
||||||
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer" :episodeNumber="results.episode"
|
:imdbId="results.media.imdbId"
|
||||||
|
:season="results.season"
|
||||||
|
:perPage="20"
|
||||||
|
:pageNumber="1"
|
||||||
|
:tmdbId="results.media.tmdbId"
|
||||||
|
:title="results.media.title"
|
||||||
|
:episodeNumber="results.episode"
|
||||||
/>
|
/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
||||||
|
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
||||||
|
{% for media in results.relatedMedia %}
|
||||||
|
<twig:Poster imdbId="{{ media.imdbId }}"
|
||||||
|
tmdbId="{{ media.tmdbId }}"
|
||||||
|
title="{{ media.title }}"
|
||||||
|
description="{{ media.description }}"
|
||||||
|
image="{{ media.poster }}"
|
||||||
|
year="{{ media.year }}"
|
||||||
|
mediaType="{{ media.mediaType }}"
|
||||||
|
/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -36,4 +36,25 @@
|
|||||||
</form>
|
</form>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||||
|
<twig:Card title="Calendar Preferences" class="w-full">
|
||||||
|
<p class="text-gray-50 mb-4">Manage your Upcoming Episodes calendar.</p>
|
||||||
|
<form id="calendar_preferences" class="flex flex-col" name="calendar_preferences" method="post" action="{{ path('app.save.calendar-preferences') }}">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row gap-2 mb-1">
|
||||||
|
<input type="hidden" name="enable_ical_up_ep" id="enable_ical_up_ep_hidden" value="0" />
|
||||||
|
<input type="checkbox" name="enable_ical_up_ep" id="enable_ical_up_ep" value="1" {{ calendarPreferences['enable_ical_up_ep'].getPreferenceValue() == true ? 'checked' }} />
|
||||||
|
<label class="text-gray-50" for="enable_ical_up_ep">Enable a publicly available iCal calendar?</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-gray-50 mb-4">Enabling the iCal calendar will allow you to subscribe from iCal
|
||||||
|
supporting clients. This endpoint will be publicly available with no authentication required.
|
||||||
|
Disabling this option will disable the calendar and public endpoint for your user.
|
||||||
|
This will not affect the calendar within the app.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="px-1.5 py-1 max-w-20 rounded-md bg-green-600 text-white" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -9,7 +9,7 @@ use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
|||||||
use App\Monitor\Action\Result\MonitorTvShowResult;
|
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\Tmdb\Tmdb;
|
use App\Tmdb\TmdbClient;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -22,7 +22,7 @@ class MonitorTvShowHandlerTest extends TestCase
|
|||||||
private MonitorTvEpisodeHandler $episodeHandler;
|
private MonitorTvEpisodeHandler $episodeHandler;
|
||||||
private MediaFiles $mediaFiles;
|
private MediaFiles $mediaFiles;
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
private Tmdb $tmdb;
|
private TmdbClient $tmdb;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,7 @@ class MonitorTvShowHandlerTest extends TestCase
|
|||||||
$this->episodeHandler = $this->createMock(MonitorTvEpisodeHandler::class);
|
$this->episodeHandler = $this->createMock(MonitorTvEpisodeHandler::class);
|
||||||
$this->mediaFiles = $this->createMock(MediaFiles::class);
|
$this->mediaFiles = $this->createMock(MediaFiles::class);
|
||||||
$this->logger = $this->createMock(LoggerInterface::class);
|
$this->logger = $this->createMock(LoggerInterface::class);
|
||||||
$this->tmdb = $this->createMock(Tmdb::class);
|
$this->tmdb = $this->createMock(TmdbClient::class);
|
||||||
|
|
||||||
$this->handler = new MonitorTvShowHandler(
|
$this->handler = new MonitorTvShowHandler(
|
||||||
$this->monitorRepository,
|
$this->monitorRepository,
|
||||||
|
|||||||
Reference in New Issue
Block a user