Compare commits

...

4 Commits

Author SHA1 Message Date
Brock H Caldwell
2315b995e0 fix: adds test exception 2025-11-08 12:40:09 -06:00
Brock H Caldwell
83401f2a7a fix: disable sentry traces 2025-11-08 12:39:01 -06:00
Brock H Caldwell
50bcb4e1df feat(Tmdb): adds pre-filter option to filter by media language 2025-11-08 12:34:53 -06:00
Brock H Caldwell
ea569b480d fix: download season handler 2025-11-07 23:24:59 -06:00
6 changed files with 103 additions and 23 deletions

5
.env
View File

@@ -62,3 +62,8 @@ NTFY_DSN=
###> sentry/sentry-symfony ### ###> sentry/sentry-symfony ###
SENTRY_DSN= SENTRY_DSN=
###< sentry/sentry-symfony ### ###< sentry/sentry-symfony ###
# TMDB 'with_original_language' option
# - only include media originally
# produced in this language
TMDB_ORIGINAL_LANGUAGE=en

View File

@@ -5,18 +5,18 @@ when@prod:
options: options:
release: '%app.version%' release: '%app.version%'
traces_sample_rate: 1 traces_sample_rate: 0
profiles_sample_rate: 1 profiles_sample_rate: 0
attach_stacktrace: true attach_stacktrace: true
tracing: # tracing:
enabled: true # enabled: true
dbal: # DB queries # dbal: # DB queries
enabled: true # enabled: true
cache: # cache pools # cache: # cache pools
enabled: true # enabled: true
twig: # templating engine # twig: # templating engine
enabled: true # enabled: true
services: services:
# (Optionally) Configure the breadcrumb handler as a service (needed for the breadcrumb Monolog handler) # (Optionally) Configure the breadcrumb handler as a service (needed for the breadcrumb Monolog handler)

View File

@@ -50,7 +50,7 @@ final class IndexController extends AbstractController
#[Route('/test')] #[Route('/test')]
public function monitorTvShow(MonitorTvShowHandler $handler): Response public function monitorTvShow(MonitorTvShowHandler $handler): Response
{ {
// $handler->handle(new MonitorTvShowCommand(82)); throw new \Exception('Test');
return $this->render('index/test.html.twig', []); return $this->render('index/test.html.twig', []);
} }
} }

View File

@@ -46,14 +46,14 @@ readonly class DownloadSeasonHandler implements HandlerInterface
$downloadCommands = []; $downloadCommands = [];
foreach ($episodesInSeason as $episode) { foreach ($episodesInSeason as $episode) {
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode['episode_number']); $this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode->episodeNumber);
$results = $this->getTvShowOptionsHandler->handle( $results = $this->getTvShowOptionsHandler->handle(
new GetTvShowOptionsCommand( new GetTvShowOptionsCommand(
$series->tmdbId, $series->tmdbId,
$command->imdbId, $command->imdbId,
$command->season, $command->season,
$episode['episode_number'] $episode->episodeNumber
) )
); );
@@ -67,7 +67,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
if (null !== $result) { if (null !== $result) {
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result'); $this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode['episode_number']); $this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode->episodeNumber);
$downloadCommand = new DownloadMediaCommand( $downloadCommand = new DownloadMediaCommand(
$result->url, $result->url,
$series->title, $series->title,
@@ -99,10 +99,10 @@ readonly class DownloadSeasonHandler implements HandlerInterface
->filter(fn ($episode) => ->filter(fn ($episode) =>
property_exists($episode, 'episode') property_exists($episode, 'episode')
&& property_exists($episode, 'season') && property_exists($episode, 'season')
&& null !== $episode->episode && null !== $episode->episodeNumber
&& null !== $episode->season && null !== $episode->season
) )
->rekey(fn($episode) => $episode->episode); ->rekey(fn($episode) => $episode->episodeNumber);
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle()); $this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
} }
} }

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Download\Action\Handler;
use Aimeos\Map;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Tmdb\Dto\TmdbEpisodeDto;
use App\Tmdb\TmdbClient;
use Carbon\Carbon;
use DateTimeImmutable;
use Psr\Log\LoggerInterface;
trait MonitorHandlerTrait
{
private MonitorRepository $monitorRepository;
private LoggerInterface $logger;
private TmdbClient $tmdb;
private function episodeReleasedAfterMonitorCreated(
string|DateTimeImmutable $monitorStartDate,
TmdbEpisodeDto $episodeInShow
): bool {
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
return $episodeAirDate >= $monitorStartDate;
}
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
{
return $downloadedEpisodes->filter(
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
&& $episode->season === $episodeInShow->seasonNumber
)->count() > 0;
}
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
{
return $this->monitorRepository->findOneBy([
'imdbId' => $monitor->getImdbId(),
'title' => $monitor->getTitle(),
'monitorType' => 'tvepisode',
'season' => $episode->seasonNumber,
'episode' => $episode->episodeNumber,
'status' => ['New', 'Active', 'In Progress']
]) !== null;
}
private function refreshData(Monitor $monitor)
{
if (null === $monitor->getPoster()) {
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster;
if (null !== $poster && "" !== $poster) {
$monitor->setPoster($poster);
}
}
}
}

View File

@@ -19,6 +19,7 @@ use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener; use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
use Tmdb\Event\Listener\Request\UserAgentRequestListener; use Tmdb\Event\Listener\Request\UserAgentRequestListener;
use Tmdb\Event\RequestEvent; use Tmdb\Event\RequestEvent;
use Tmdb\Repository\DiscoverRepository;
use Tmdb\Repository\MovieRepository; use Tmdb\Repository\MovieRepository;
use Tmdb\Repository\SearchRepository; use Tmdb\Repository\SearchRepository;
use Tmdb\Repository\TvEpisodeRepository; use Tmdb\Repository\TvEpisodeRepository;
@@ -30,6 +31,7 @@ use Tmdb\Token\Api\BearerToken;
class TmdbClient class TmdbClient
{ {
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500"; const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
const APPEND_TO_RESPONSE = 'external_ids,credits,watch/providers';
protected Client $client; protected Client $client;
protected MovieRepository $movieRepository; protected MovieRepository $movieRepository;
@@ -38,6 +40,8 @@ class TmdbClient
protected TvEpisodeRepository $tvEpisodeRepository; protected TvEpisodeRepository $tvEpisodeRepository;
protected SearchRepository $searchRepository; protected SearchRepository $searchRepository;
protected DiscoverRepository $discoverRepository;
protected array $mediaTypeMap = [ protected array $mediaTypeMap = [
MediaType::Movie->value => MediaType::Movie->value, MediaType::Movie->value => MediaType::Movie->value,
MediaType::TvShow->value => MediaType::TvShow->value, MediaType::TvShow->value => MediaType::TvShow->value,
@@ -48,11 +52,14 @@ class TmdbClient
protected $repos = []; protected $repos = [];
private ?string $originalLanguage = 'en';
public function __construct( public function __construct(
private readonly SerializerInterface $serializer, private readonly SerializerInterface $serializer,
private readonly CacheItemPoolInterface $cache, private readonly CacheItemPoolInterface $cache,
private readonly EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
#[Autowire(env: 'TMDB_API')] string $apiKey, #[Autowire(env: 'TMDB_API')] string $apiKey,
#[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null,
) { ) {
$this->client = new Client( $this->client = new Client(
[ [
@@ -74,7 +81,7 @@ class TmdbClient
'hydration' => [ 'hydration' => [
'event_listener_handles_hydration' => false, 'event_listener_handles_hydration' => false,
'only_for_specified_models' => [] 'only_for_specified_models' => []
] ],
] ]
); );
@@ -107,11 +114,14 @@ class TmdbClient
$this->tvSeasonRepository = new TvSeasonRepository($this->client); $this->tvSeasonRepository = new TvSeasonRepository($this->client);
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client); $this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
$this->searchRepository = new SearchRepository($this->client); $this->searchRepository = new SearchRepository($this->client);
$this->discoverRepository = new DiscoverRepository($this->client);
$this->repos = [ $this->repos = [
MediaType::Movie->value => $this->movieRepository, MediaType::Movie->value => $this->movieRepository,
MediaType::TvShow->value => $this->tvRepository, MediaType::TvShow->value => $this->tvRepository,
MediaType::TvEpisode->value => $this->tvEpisodeRepository, MediaType::TvEpisode->value => $this->tvEpisodeRepository,
]; ];
$this->originalLanguage = $originalLanguage;
} }
public function search(string $term): TmdbResult|Map public function search(string $term): TmdbResult|Map
@@ -133,7 +143,7 @@ class TmdbClient
{ {
$tmdbId = $this->findByImdbId($imdbId)['id']; $tmdbId = $this->findByImdbId($imdbId)['id'];
return $this->parseResult( return $this->parseResult(
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids,credits']), $this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]),
MediaType::Movie->value, MediaType::Movie->value,
$imdbId $imdbId
); );
@@ -142,7 +152,7 @@ class TmdbClient
public function tvshowDetails(string $imdbId): ?TmdbResult public function tvshowDetails(string $imdbId): ?TmdbResult
{ {
$tmdbId = $this->findByImdbId($imdbId)['id']; $tmdbId = $this->findByImdbId($imdbId)['id'];
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids,credits']); $media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]);
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) { $media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
return $data['season_number'] !== 0 && return $data['season_number'] !== 0 &&
@@ -163,7 +173,7 @@ class TmdbClient
public function tvSeasonDetails(string $tmdbId, int $season): array public function tvSeasonDetails(string $tmdbId, int $season): array
{ {
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => 'external_ids,credits']); $result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => static::APPEND_TO_RESPONSE]);
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) { $result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path']; $data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
$data['poster'] = $data['still_path']; $data['poster'] = $data['still_path'];
@@ -174,7 +184,7 @@ class TmdbClient
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
{ {
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']); $result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => static::APPEND_TO_RESPONSE]);
return $this->parseResult( return $this->parseResult(
$result, $result,
MediaType::TvEpisode->value, MediaType::TvEpisode->value,
@@ -193,7 +203,10 @@ class TmdbClient
public function popularMovies(int $resultCount = 6): Map public function popularMovies(int $resultCount = 6): Map
{ {
$results = $this->movieRepository->getApi()->getPopular(); $results = $this->discoverRepository->getApi()->discoverMovies([
'with_original_language' => $this->originalLanguage,
'append_to_response' => 'external_ids,watch/providers',
]);
$results['results'] = Map::from($results['results'])->map(function ($result) { $results['results'] = Map::from($results['results'])->map(function ($result) {
$result['media_type'] = MediaType::Movie->value; $result['media_type'] = MediaType::Movie->value;
return $result; return $result;
@@ -206,7 +219,10 @@ class TmdbClient
public function popularTvShows(int $resultCount = 6): Map public function popularTvShows(int $resultCount = 6): Map
{ {
$results = $this->tvRepository->getApi()->getPopular(); $results = $this->discoverRepository->getApi()->discoverTv([
'with_original_language' => $this->originalLanguage,
'append_to_response' => 'external_ids,watch/providers',
]);
$results['results'] = Map::from($results['results'])->map(function ($result) { $results['results'] = Map::from($results['results'])->map(function ($result) {
$result['media_type'] = MediaType::TvShow->value; $result['media_type'] = MediaType::TvShow->value;
return $result; return $result;