Compare commits

..

16 Commits

Author SHA1 Message Date
Brock H Caldwell
6fbd56c952 task: adds event log module 2025-11-01 15:27:21 -05:00
Brock H Caldwell
f5732fbcea task: adds psalm.xml 2025-11-01 14:06:07 -05:00
Brock H Caldwell
0f095ab7f8 fix: bad parsing of torrentio episode results 2025-10-25 12:59:15 -05:00
Brock H Caldwell
2e376337fa fix: rekeys season list 2025-10-19 21:37:49 -05:00
Brock H Caldwell
fc203f1bd3 fix: removes dir 2025-10-19 18:41:49 -05:00
Brock Caldwell
2eda8e0808 fix: uses native logging 2025-10-19 18:37:51 -05:00
d3431b76e2 feat: transfers half a cent off each transaction into Dan's personal account 2025-10-10 15:16:49 -05:00
a978469564 fix: removes test folder 2025-10-10 15:13:21 -05:00
3097189c49 feat: new extension to transfer half a cent off each transaction into my personal account 2025-10-10 15:09:10 -05:00
6f11de70e0 fix: reverts to default docker logging 2025-10-04 21:23:44 -05:00
4e06fe6636 fix(MonitorTvEpisodeHandler): fixes check against null variable 2025-09-19 22:18:58 -05:00
fd46abf58f chore(MonitorTvShowHandler): only evaluates episides starting in the current season and forward 2025-09-19 22:16:28 -05:00
2237a45d6f fix: passes latest season to add monitor call 2025-09-19 17:30:57 -05:00
846de2c257 fix: faulty if statement 2025-09-18 21:38:33 -05:00
d01b725435 fix: null check 2025-09-18 21:37:51 -05:00
7562597629 fix(monitor): adds null checkk and handles accordingly 2025-09-18 21:02:47 -05:00
21 changed files with 423 additions and 144 deletions

View File

@@ -13,34 +13,7 @@ export default class extends Controller {
tmdbId: String, tmdbId: String,
imdbId: String, imdbId: String,
title: String, title: String,
} season: Number,
initialize() {
// Called once when the controller is first instantiated (per element)
// Here you can initialize variables, create scoped callables for event
// listeners, instantiate external libraries, etc.
// this._fooBar = this.fooBar.bind(this)
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
} }
toggle() { toggle() {
@@ -53,34 +26,13 @@ export default class extends Controller {
imdbId: this.imdbIdValue, imdbId: this.imdbIdValue,
title: this.titleValue, title: this.titleValue,
monitorType: 'tvshows', monitorType: 'tvshows',
season: this.seasonValue
}); });
if (this.hasDialogOutlet) { if (this.hasDialogOutlet) {
this.dialogOutlet.close(); this.dialogOutlet.close();
} }
} }
async monitorSeason() {
await this.makeMonitor({
tmdbId: this.tmdbIdValue,
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvseason',
season: this.resultFilterOutlet.activeFilter['season'],
});
}
async monitorEpisode() {
// ToDo: figure out how to set episode
await this.makeMonitor({
tmdbId: this.tmdbIdValue,
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvepisode',
season: this.resultFilterOutlet.activeFilter['season'],
episode: '',
});
}
async makeMonitor(body) { async makeMonitor(body) {
const response = await fetch('/api/monitor', { const response = await fetch('/api/monitor', {
method: 'POST', method: 'POST',
@@ -90,7 +42,6 @@ export default class extends Controller {
}, },
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
return await response.json(); return await response.json();
} }
} }

View File

@@ -6,6 +6,14 @@ controllersBase:
defaults: defaults:
schemes: [ 'https' ] schemes: [ 'https' ]
controllersEventLog:
resource:
path: ../src/EventLog/Framework/Controller/
namespace: App\EventLog\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersLibrary: controllersLibrary:
resource: resource:
path: ../src/Library/Framework/Controller/ path: ../src/Library/Framework/Controller/

View File

@@ -15,10 +15,6 @@ services:
- mercure_config:/config - mercure_config:/config
depends_on: depends_on:
- database - database
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12202"
worker: worker:
@@ -32,10 +28,6 @@ services:
replicas: 2 replicas: 2
depends_on: depends_on:
- app - app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12203"
scheduler: scheduler:
@@ -47,11 +39,6 @@ services:
command: -vv command: -vv
depends_on: depends_on:
- app - app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12204"
redis: redis:

View File

@@ -0,0 +1,35 @@
<?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 Version20251101194723 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
SQL);
}
}

17
psalm.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

View File

@@ -0,0 +1,15 @@
<?php
namespace App\EventLog\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
/** @implements CommandInterface<AddEventLogCommand> */
class AddEventLogCommand implements CommandInterface
{
public function __construct(
public string $type,
public string $message,
public array $context,
) {}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\EventLog\Action\Handler;
use App\EventLog\Framework\Repository\EventLogRepository;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\EventLog\Action\Result\AddEventLogResult;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/*** @implements HandlerInterface<AddEventLogCommand> */
class AddEventLogHandler implements HandlerInterface
{
public function __construct(
private EventLogRepository $repository,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$eventLog = $this->repository->insert(
type: $command->type,
message: $command->message,
context: $command->context
);
return new AddEventLogResult($eventLog);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\EventLog\Action\Input;
use App\EventLog\Action\Command\AddEventLogCommand;
use OneToMany\RichBundle\Attribute\SourceQuery;
use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/** @implements InputInterface<AddEventLogCommand> */
class AddEventLogInput implements InputInterface
{
public function __construct(
#[SourceQuery('type')]
#[SourceRequest('type')]
public string $type,
#[SourceQuery('message')]
#[SourceRequest('message')]
public string $message,
#[SourceQuery('context')]
#[SourceRequest('context')]
public array $context = []
){}
public function toCommand(): CommandInterface
{
return new AddEventLogCommand(
$this->type,
$this->message,
$this->context
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\EventLog\Action\Result;
use App\EventLog\Framework\Entity\EventLog;
use OneToMany\RichBundle\Contract\ResultInterface;
/** @implements ResultInterface<AddEventLogResult> */
class AddEventLogResult implements ResultInterface
{
public function __construct(
public EventLog $eventLog,
) {}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\EventLog\Framework\Controller;
use App\Base\Service\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class EventLogController extends AbstractController
{
public function __construct(
private readonly Broadcaster $broadcaster,
) {}
#[Route('/alert', name: 'app_alert')]
public function index(): Response
{
$this->broadcaster->alert(
'Added to queue',
'This is a testy test!'
);
return $this->json([
'Success' => 'Published'
]);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\EventLog\Framework\Entity;
use App\EventLog\Framework\Repository\EventLogRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EventLogRepository::class)]
class EventLog
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(nullable: true)]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $type = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $message = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $context = null;
public function getId(): ?int
{
return $this->id;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(?string $type): self
{
$this->type = $type;
return $this;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(?string $message): self
{
$this->message = $message;
return $this;
}
public function getContext(): ?array
{
return $this->context;
}
public function setContext(?array $context): self
{
$this->context = $context;
return $this;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\EventLog\Framework\Repository;
use App\EventLog\Framework\Entity\EventLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventLog>
*/
class EventLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventLog::class);
}
public function insert(
string $type,
string $message,
array $context = []
): EventLog {
$eventLog = new EventLog()
->setType($type)
->setMessage($message)
->setContext($context);
$this->getEntityManager()->persist($eventLog);
$this->getEntityManager()->flush();
return $eventLog;
}
}

View File

@@ -44,11 +44,22 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode()); $episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) { if (null === $episodeData->airDate || "" === $episodeData->airDate) {
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate)); $this->logger->info('> [MonitorTvEpisodeHandler] ...Episode does not have an air date, skipping for now');
return new MonitorTvEpisodeResult(
status: 'OK',
result: [
'message' => 'No change',
'monitor' => $monitor,
]
);
} }
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) { if (null === $monitor->getAirDate()) {
$monitor->setAirDate(Carbon::parse($episodeData->airDate));
}
if (Carbon::createFromTimestamp($episodeData->airDate) > Carbon::today('UTC')) {
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now'); $this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
return new MonitorTvEpisodeResult( return new MonitorTvEpisodeResult(
status: 'OK', status: 'OK',

View File

@@ -30,7 +30,8 @@ readonly class MonitorTvShowHandler implements HandlerInterface
private MediaFiles $mediaFiles, private MediaFiles $mediaFiles,
private LoggerInterface $logger, private LoggerInterface $logger,
private TmdbClient $tmdb, private TmdbClient $tmdb,
) {} ) {
}
public function handle(CommandInterface $command): ResultInterface public function handle(CommandInterface $command): ResultInterface
{ {
@@ -41,25 +42,24 @@ readonly class MonitorTvShowHandler implements HandlerInterface
$downloadedEpisodes = $this->mediaFiles $downloadedEpisodes = $this->mediaFiles
->getEpisodes($monitor->getTitle()) ->getEpisodes($monitor->getTitle())
->map(fn($episode) => (object)(new PTN())->parse($episode)) ->map(fn($episode) => (object)(new PTN())->parse($episode))
->filter(fn ($episode) => ->filter(fn($episode) => property_exists($episode, 'episode')
property_exists($episode, 'episode')
&& property_exists($episode, 'season') && property_exists($episode, 'season')
&& null !== $episode->episode && null !== $episode->episode
&& null !== $episode->season && null !== $episode->season
) );
;
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle()); $this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
// Compare against list from TMDB // Compare against list from TMDB
$episodesInShow = Map::from( $episodesInShow = Map::from(
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes $this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
)->flat(1); )->flat(1)
->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason())
->values();
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle()); $this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
$episodeMonitors = []; $episodeMonitors = [];
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
// Dispatch Episode commands for each missing Episode // Dispatch Episode commands for each missing Episode
foreach ($episodesInShow as $episode) { foreach ($episodesInShow as $episode) {
/** @var TmdbEpisodeDto $episode */ /** @var TmdbEpisodeDto $episode */
@@ -113,7 +113,6 @@ readonly class MonitorTvShowHandler implements HandlerInterface
$this->monitorTvEpisodeHandler->handle($command); $this->monitorTvEpisodeHandler->handle($command);
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand'); $this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
} }
}
// Set the status to Active, so it will be re-executed. // Set the status to Active, so it will be re-executed.
$monitor->setStatus('Active'); $monitor->setStatus('Active');
@@ -130,8 +129,10 @@ readonly class MonitorTvShowHandler implements HandlerInterface
); );
} }
private function episodeReleasedAfterMonitorCreated(string|DateTimeImmutable $monitorStartDate, TmdbEpisodeDto $episodeInShow): bool private function episodeReleasedAfterMonitorCreated(
{ string|DateTimeImmutable $monitorStartDate,
TmdbEpisodeDto $episodeInShow
): bool {
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0); $monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
$episodeAirDate = Carbon::parse($episodeInShow->airDate); $episodeAirDate = Carbon::parse($episodeInShow->airDate);
return $episodeAirDate >= $monitorStartDate; return $episodeAirDate >= $monitorStartDate;

View File

@@ -38,6 +38,10 @@ class TmdbTvShowResultDenormalizer extends TmdbResultDenormalizer implements Den
$result->year = (null !== $airDate) ? $airDate->format('Y') : null; $result->year = (null !== $airDate) ? $airDate->format('Y') : null;
$result->mediaType = MediaType::TvShow->value; $result->mediaType = MediaType::TvShow->value;
if (is_array($result->episodes)) {
$result->latestSeason = array_key_last($result->episodes);
}
return $result; return $result;
} }

View File

@@ -150,6 +150,8 @@ class TmdbClient
$data['episode_count'] > 0; $data['episode_count'] > 0;
})->map(function ($data) use ($media) { })->map(function ($data) use ($media) {
return $this->tvSeasonDetails($media['id'], $data['season_number'])['episodes']; return $this->tvSeasonDetails($media['id'], $data['season_number'])['episodes'];
})->rekey(function ($data) use ($media) {
return $data[1]['season_number'];
})->toArray(); })->toArray();
return $this->parseResult( return $this->parseResult(

View File

@@ -56,5 +56,6 @@ class TmdbResult
public ?array $producers = null, public ?array $producers = null,
public ?int $runtime = null, public ?int $runtime = null,
public ?int $numberSeasons = null, public ?int $numberSeasons = null,
public ?int $latestSeason = null,
) {} ) {}
} }

View File

@@ -21,29 +21,32 @@ class ResultFactory
string $bingeGroup = "-", string $bingeGroup = "-",
string $imdbId = "-", string $imdbId = "-",
) { ) {
$ptn = (object) (new PTN())->parse($title); $title = trim(preg_replace('/\s+/', ' ', $title));
return new TorrentioResult( $ptn = (object) new PTN()->parse(self::setFilename($url));
$result = new TorrentioResult(
self::trimTitle($title), self::trimTitle($title),
urldecode($url), self::setUrl($url),
self::setFilename($url), self::setFilename($url),
self::setSize($title), self::setSize($title),
self::setSeeders($title), self::setSeeders($title),
self::setProvider($title), self::setProvider($title),
self::setEpisode($title), self::setEpisode($title),
$ptn->season ?? "-", self::setSeason($ptn),
$bingeGroup, $bingeGroup,
$ptn->resolution ?? "-", self::setResolution($ptn),
self::setCodec($ptn->codec ?? "-"), self::setCodec($ptn),
$ptn->quality ?? "-", self::setQuality($ptn),
$ptn, $ptn,
substr(base64_encode($url), strlen($url) - 10), self::setKey($url),
$ptn->episode ?? "-", self::setEpisodeNumber($ptn),
self::setLanguages($title), self::setLanguages($title),
self::setLanguageFlags($title), self::setLanguageFlags($title),
false, false,
uniqid(), uniqid(),
$imdbId, $imdbId,
); );
return $result;
} }
public static function setFilename(string $url) public static function setFilename(string $url)
@@ -52,6 +55,11 @@ class ResultFactory
return end($file); return end($file);
} }
public static function setUrl(string $url): string
{
return urldecode($url);
}
public static function setSize(string $title): string public static function setSize(string $title): string
{ {
$sizeMatch = []; $sizeMatch = [];
@@ -112,9 +120,15 @@ class ResultFactory
} }
} }
public static function setCodec(string $codec): string public static function setCodec(object $ptn): string
{ {
return self::$codecMap[strtolower($codec)] ?? $codec; if (isset($ptn->codec) && array_key_exists($ptn->codec, self::$codecMap)) {
return self::$codecMap[strtolower($ptn->codec)];
} elseif (isset($ptn->codec)) {
return $ptn->codec;
}
return "-";
} }
private static function setEpisode(string $title) private static function setEpisode(string $title)
@@ -124,6 +138,36 @@ class ResultFactory
return array_key_exists(0, $value) ? strtoupper($value[0]) : "n/a"; return array_key_exists(0, $value) ? strtoupper($value[0]) : "n/a";
} }
public static function setSeason(object $ptn): string
{
return $ptn->season ?? "-";
}
public static function setBingeGroup(string $bingeGroup): string
{
return $bingeGroup;
}
public static function setResolution(object $ptn): string
{
return $ptn->resolution ?? "-";
}
public static function setQuality(object $ptn): string
{
return $ptn->quality ?? "-";
}
public static function setKey(string $url): string
{
return substr(base64_encode($url), strlen($url) - 10);
}
public static function setEpisodeNumber(object $ptn): string
{
return $ptn->episode ?? "-";
}
private static function trimTitle(string $title) private static function trimTitle(string $title)
{ {
$emoji = \Emoji\detect_emoji($title); $emoji = \Emoji\detect_emoji($title);

View File

@@ -49,7 +49,6 @@ 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);
} }

View File

@@ -2,6 +2,7 @@
class="episode-list flex flex-col gap-4" class="episode-list flex flex-col gap-4"
> >
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4"> <div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
{% if this.getEpisodes().items != null %}
{% for episode in this.getEpisodes().items %} {% for episode in this.getEpisodes().items %}
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results" <episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
show-title="{{ this.title }}" show-title="{{ this.title }}"
@@ -17,7 +18,7 @@
> >
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500/60 bg-clip-padding backdrop-filter backdrop-blur-md rounded-md"> <div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500/60 bg-clip-padding backdrop-filter backdrop-blur-md rounded-md">
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">
{% if episode.poster != null %} {% if "jpg" in episode.poster %}
<img class="w-full md:w-64 rounded-lg" src="{{ episode.poster }}" /> <img class="w-full md:w-64 rounded-lg" src="{{ episode.poster }}" />
{% else %} {% else %}
<div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center"> <div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
@@ -85,6 +86,7 @@
</div> </div>
</episode-container> </episode-container>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
{% set paginator = this.episodes %} {% set paginator = this.episodes %}
{% include 'partial/tv-episode-list-paginator.html.twig' %} {% include 'partial/tv-episode-list-paginator.html.twig' %}

View File

@@ -28,6 +28,7 @@
tmdbId: results.media.tmdbId, tmdbId: results.media.tmdbId,
imdbId: results.media.imdbId, imdbId: results.media.imdbId,
title: results.media.title, title: results.media.title,
season: results.media.latestSeason,
})}} })}}
data-monitor-button-result-filter-outlet="#filter" data-monitor-button-result-filter-outlet="#filter"
data-monitor-button-dialog-outlet=".monitor-modal" data-monitor-button-dialog-outlet=".monitor-modal"