Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca89eff236 | |||
| 53da7a746b | |||
| 981699bc13 | |||
| 52f460ff62 | |||
| a42e0d4d1a | |||
| 3e4a2d9bb1 | |||
| af8a30826c | |||
| 09e1c75826 | |||
| f1b8b34359 | |||
| 6f9db68664 | |||
| aeb706b5af | |||
| 7918c260e5 | |||
| 38130ea0ec | |||
| da403958dc | |||
| c2bafabb20 | |||
| e6983aedf9 | |||
| e9edd6a35a | |||
| ee076518b3 | |||
| a2f16398be | |||
| 0f03199eb4 | |||
| d63d477ed1 | |||
| 458229c7ed | |||
| 6748188256 | |||
| b42924048f |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.phpunit.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.env.test
|
||||||
|
.gitignore
|
||||||
|
bolt.db
|
||||||
|
bash
|
||||||
|
build.xml
|
||||||
|
deploy.compose.yml
|
||||||
|
phpstan.dist.neon
|
||||||
|
phpunit.dist.xml
|
||||||
|
nomad.deploy.hcl
|
||||||
|
deployment.properties
|
||||||
7
bash/build_base.sh
Executable file
7
bash/build_base.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
# torsearch-app is built from this base, and torsearch-worker & torsearch-scheduler are built from torsearch-app
|
||||||
|
# This image is used to speed up build times
|
||||||
|
export FRANKENPHP_TAG=php8.4
|
||||||
|
|
||||||
|
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base -t code.caldwell.digital/home/torsearch-base:${FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${FRANKENPHP_TAG}" .
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base:${FRANKENPHP_TAG}
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base:latest
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
"p3k/emoji-detector": "^1.2",
|
"p3k/emoji-detector": "^1.2",
|
||||||
"php-http/cache-plugin": "^2.0",
|
"php-http/cache-plugin": "^2.0",
|
||||||
"php-tmdb/api": "^4.1",
|
"php-tmdb/api": "^4.1",
|
||||||
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"predis/predis": "^2.4",
|
"predis/predis": "^2.4",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
"spatie/icalendar-generator": "^3.0",
|
"spatie/icalendar-generator": "^3.0",
|
||||||
@@ -47,9 +49,12 @@
|
|||||||
"symfony/notifier": "7.3.*",
|
"symfony/notifier": "7.3.*",
|
||||||
"symfony/ntfy-notifier": "7.3.*",
|
"symfony/ntfy-notifier": "7.3.*",
|
||||||
"symfony/object-mapper": "7.3.*",
|
"symfony/object-mapper": "7.3.*",
|
||||||
|
"symfony/property-access": "7.3.*",
|
||||||
|
"symfony/property-info": "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.*",
|
||||||
|
"symfony/serializer": "7.3.*",
|
||||||
"symfony/stimulus-bundle": "^2.24",
|
"symfony/stimulus-bundle": "^2.24",
|
||||||
"symfony/twig-bundle": "7.3.*",
|
"symfony/twig-bundle": "7.3.*",
|
||||||
"symfony/ux-autocomplete": "^2.27",
|
"symfony/ux-autocomplete": "^2.27",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ parameters:
|
|||||||
app.cache.redis.host.default: 'redis://redis'
|
app.cache.redis.host.default: 'redis://redis'
|
||||||
|
|
||||||
# Various configs
|
# Various configs
|
||||||
app.default.version: '0.dev'
|
app.default.version: '0.0.0-dev'
|
||||||
app.default.timezone: 'America/Chicago'
|
app.default.timezone: 'America/Chicago'
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
@@ -67,8 +67,6 @@ 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:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: registry.caldwell.digital/home/torsearch-app:${TAG}
|
image: registry.caldwell.digital/home/torsearch-app:${TAG}
|
||||||
ports:
|
ports:
|
||||||
- '8001:80'
|
- "${SWARM_PORT}:80"
|
||||||
environment:
|
environment:
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ templates
|
|||||||
var
|
var
|
||||||
vendor
|
vendor
|
||||||
build.xml
|
build.xml
|
||||||
|
.git
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
composer.json
|
composer.json
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
FROM dunglas/frankenphp
|
FROM code.caldwell.digital/home/torsearch-base:php8.4
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||||
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||||
|
|||||||
14
docker/Dockerfile.base.app
Normal file
14
docker/Dockerfile.base.app
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
ARG FRANKENPHP_TAG
|
||||||
|
|
||||||
|
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
||||||
|
|
||||||
|
ENV SERVER_NAME=":80"
|
||||||
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
|
|
||||||
|
RUN install-php-extensions \
|
||||||
|
pdo_mysql \
|
||||||
|
gd \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
opcache
|
||||||
19
docker/Dockerfile.base.worker
Normal file
19
docker/Dockerfile.base.worker
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
ARG FRANKENPHP_TAG
|
||||||
|
|
||||||
|
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
||||||
|
|
||||||
|
ENV SERVER_NAME=":80"
|
||||||
|
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
|
||||||
|
|
||||||
|
RUN apk add --no-cache wget
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
ARG APP_VERSION
|
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
||||||
|
|
||||||
FROM code.caldwell.digital/torsearch/torsearch-app:${APP_VERSION}
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
FROM dunglas/frankenphp:php8.4-alpine
|
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
||||||
|
|||||||
@@ -2,16 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Base;
|
namespace App\Base;
|
||||||
|
|
||||||
|
use App\Base\Dto\AppVersionDto;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
|
||||||
final class ConfigResolver
|
final class ConfigResolver
|
||||||
{
|
{
|
||||||
|
const SEMVER_REGEX = '/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/';
|
||||||
|
|
||||||
private array $messages = [];
|
private array $messages = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly DenormalizerInterface $denormalizer,
|
||||||
|
|
||||||
#[Autowire(param: 'app.url')]
|
#[Autowire(param: 'app.url')]
|
||||||
private readonly ?string $appUrl = null,
|
private readonly ?string $appUrl = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'app.version')]
|
||||||
|
private readonly ?string $appVersion = null,
|
||||||
|
|
||||||
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
||||||
private readonly ?string $realDebridApiKey = null,
|
private readonly ?string $realDebridApiKey = null,
|
||||||
|
|
||||||
@@ -92,6 +101,13 @@ final class ConfigResolver
|
|||||||
return $this->authOidcBypassFormLogin;
|
return $this->authOidcBypassFormLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAppVersion(): AppVersionDto
|
||||||
|
{
|
||||||
|
$matches = [];
|
||||||
|
preg_match(self::SEMVER_REGEX, $this->appVersion, $matches);
|
||||||
|
return $this->denormalizer->denormalize($matches, AppVersionDto::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function getAuthConfig(): array
|
public function getAuthConfig(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
24
src/Base/Dto/AppVersionDto.php
Normal file
24
src/Base/Dto/AppVersionDto.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Base\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class AppVersionDto
|
||||||
|
{
|
||||||
|
#[SerializedPath('[1]')]
|
||||||
|
public string|int $major = 0;
|
||||||
|
#[SerializedPath('[2]')]
|
||||||
|
public string|int $minor = 0;
|
||||||
|
#[SerializedPath('[3]')]
|
||||||
|
public string|int $patch = 0;
|
||||||
|
#[SerializedPath('[4]')]
|
||||||
|
public ?string $pre = null;
|
||||||
|
#[SerializedPath('[5]')]
|
||||||
|
public ?string $build = null;
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return 'v' . $this->major . '.' . $this->minor . '.' . $this->patch . ($this->pre ? '-' . $this->pre : '') . ($this->build ? '+' . $this->build : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
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\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
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;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
@@ -46,8 +48,9 @@ final class IndexController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/test')]
|
#[Route('/test')]
|
||||||
public function monitorTvShow(): Response
|
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
||||||
{
|
{
|
||||||
|
// $handler->handle(new MonitorTvShowCommand(82));
|
||||||
return $this->render('index/test.html.twig', []);
|
return $this->render('index/test.html.twig', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
||||||
|
|
||||||
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||||
|
|
||||||
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
||||||
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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\Dto\TmdbEpisodeDto;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -61,8 +62,9 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
|
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 */
|
||||||
// Only monitor future episodes
|
// Only monitor future episodes
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode['season_number'] . '" episode "' . $episode['episode_number'] . '"');
|
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode->seasonNumber . '" episode "' . $episode->episodeNumber . '"');
|
||||||
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
||||||
if (false === $episodeInFuture) {
|
if (false === $episodeInFuture) {
|
||||||
@@ -94,9 +96,9 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
->setImdbId($monitor->getImdbId())
|
->setImdbId($monitor->getImdbId())
|
||||||
->setTitle($monitor->getTitle())
|
->setTitle($monitor->getTitle())
|
||||||
->setMonitorType('tvepisode')
|
->setMonitorType('tvepisode')
|
||||||
->setSeason($episode['season_number'])
|
->setSeason($episode->seasonNumber)
|
||||||
->setEpisode($episode['episode_number'])
|
->setEpisode($episode->episodeNumber)
|
||||||
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
|
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
|
||||||
->setCreatedAt(new DateTimeImmutable())
|
->setCreatedAt(new DateTimeImmutable())
|
||||||
->setSearchCount(0)
|
->setSearchCount(0)
|
||||||
->setStatus('New');
|
->setStatus('New');
|
||||||
@@ -128,29 +130,29 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function episodeReleasedAfterMonitorCreated(string|DateTimeImmutable $monitorStartDate, array $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['air_date']);
|
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
||||||
return $episodeAirDate >= $monitorStartDate;
|
return $episodeAirDate >= $monitorStartDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
|
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
||||||
{
|
{
|
||||||
return $downloadedEpisodes->filter(
|
return $downloadedEpisodes->filter(
|
||||||
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
|
fn (object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||||
&& $episode->season === $episodeInShow['season_number']
|
&& $episode->season === $episodeInShow->seasonNumber
|
||||||
)->count() > 0;
|
)->count() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function monitorExists(Monitor $monitor, array $episode): bool
|
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
|
||||||
{
|
{
|
||||||
return $this->monitorRepository->findOneBy([
|
return $this->monitorRepository->findOneBy([
|
||||||
'imdbId' => $monitor->getImdbId(),
|
'imdbId' => $monitor->getImdbId(),
|
||||||
'title' => $monitor->getTitle(),
|
'title' => $monitor->getTitle(),
|
||||||
'monitorType' => 'tvepisode',
|
'monitorType' => 'tvepisode',
|
||||||
'season' => $episode['season_number'],
|
'season' => $episode->seasonNumber,
|
||||||
'episode' => $episode['episode_number'],
|
'episode' => $episode->episodeNumber,
|
||||||
'status' => ['New', 'Active', 'In Progress']
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
]) !== null;
|
]) !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class Monitor
|
|||||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||||
private ?self $parent = null;
|
private ?self $parent = null;
|
||||||
|
|
||||||
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent', cascade: ['remove'])]
|
||||||
private Collection $children;
|
private Collection $children;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
19
src/Tmdb/Dto/CastMemberDto.php
Normal file
19
src/Tmdb/Dto/CastMemberDto.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class CastMemberDto
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[SerializedPath('[name]')]
|
||||||
|
public string $name,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Tmdb/Dto/CrewMemberDto.php
Normal file
18
src/Tmdb/Dto/CrewMemberDto.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class CrewMemberDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SerializedPath('[name]')]
|
||||||
|
public string $name,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Tmdb/Dto/GenreDto.php
Normal file
18
src/Tmdb/Dto/GenreDto.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class GenreDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public string $name,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Tmdb/Dto/TmdbEpisodeDto.php
Normal file
29
src/Tmdb/Dto/TmdbEpisodeDto.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Dto;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class TmdbEpisodeDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SerializedPath('[id]')]
|
||||||
|
public ?int $tmdbId = null,
|
||||||
|
#[SerializedPath('[show_id]')]
|
||||||
|
public ?int $tmdbShowId = null,
|
||||||
|
public ?string $mediaType = MediaType::TvShow->value,
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
public ?string $name = null,
|
||||||
|
#[SerializedPath('[air_date]')]
|
||||||
|
public ?string $airDate = null,
|
||||||
|
#[SerializedPath('[overview]')]
|
||||||
|
public ?string $description = null,
|
||||||
|
public ?string $poster = null,
|
||||||
|
public ?int $runtime = 0,
|
||||||
|
#[SerializedPath('[season_number]')]
|
||||||
|
public ?int $seasonNumber = 0,
|
||||||
|
#[SerializedPath('[episode_number]')]
|
||||||
|
public ?int $episodeNumber = 0,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Framework\Serializer;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
class TmdbMovieResultDenormalizer extends 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,
|
||||||
|
) {
|
||||||
|
parent::__construct($normalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): TmdbResult|array|null
|
||||||
|
{
|
||||||
|
/** @var TmdbResult $result */
|
||||||
|
$result = parent::denormalize($data, TmdbResult::class, $format, $context);
|
||||||
|
|
||||||
|
if (array_key_exists('release_date', $data) && !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 = MediaType::Movie->value;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization(
|
||||||
|
mixed $data,
|
||||||
|
string $type,
|
||||||
|
?string $format = null,
|
||||||
|
array $context = []
|
||||||
|
): bool {
|
||||||
|
return array_key_exists('media_type', $context) &&
|
||||||
|
$context['media_type'] === MediaType::Movie->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TmdbResult::class => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/Tmdb/Framework/Serializer/TmdbResultDenormalizer.php
Normal file
105
src/Tmdb/Framework/Serializer/TmdbResultDenormalizer.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Framework\Serializer;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\Dto\CastMemberDto;
|
||||||
|
use App\Tmdb\Dto\CrewMemberDto;
|
||||||
|
use App\Tmdb\Dto\GenreDto;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): TmdbResult|array|null
|
||||||
|
{
|
||||||
|
$result = $this->normalizer->denormalize($data, TmdbResult::class, $format, $context);
|
||||||
|
$result->stars = $this->getStars($data);
|
||||||
|
$result->directors = $this->getDirectors($data);
|
||||||
|
$result->producers = $this->getProducers($data);
|
||||||
|
$result->creators = $this->getCreators($data);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getStars(array $data): ?array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('credits', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Map::from($data['credits']['cast'])
|
||||||
|
->slice(0, 3)
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, CastMemberDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDirectors(array $data): ?array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('credits', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Map::from($data['credits']['crew'])
|
||||||
|
->filter(fn($item) => $item['job'] === 'Director')
|
||||||
|
->slice(0, 3)
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, CrewMemberDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreators(array $data): ?array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('credits', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Map::from($data['credits']['crew'])
|
||||||
|
->filter(fn($item) => $item['job'] === 'Creator')
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, CrewMemberDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProducers(array $data): ?array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('credits', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Map::from($data['credits']['crew'])
|
||||||
|
->filter(fn($item) => $item['job'] === 'Producer')
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, CrewMemberDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGenres(array $data, MediaType $mediaType): ?array
|
||||||
|
{
|
||||||
|
if (array_key_exists('genres', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Map::from($data['genres'])
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, GenreDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization(
|
||||||
|
mixed $data,
|
||||||
|
string $type,
|
||||||
|
?string $format = null,
|
||||||
|
array $context = []
|
||||||
|
): bool {
|
||||||
|
return !array_key_exists('media_type', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TmdbResult::class => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Framework\Serializer;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
class TmdbTvEpisodeResultDenormalizer implements DenormalizerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(service: 'serializer.normalizer.object')]
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): TmdbResult|TmdbEpisodeDto|array|null
|
||||||
|
{
|
||||||
|
/** @var TmdbEpisodeDto $result */
|
||||||
|
$result = $this->normalizer->denormalize($data, TmdbEpisodeDto::class, $format, $context);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization(
|
||||||
|
mixed $data,
|
||||||
|
string $type,
|
||||||
|
?string $format = null,
|
||||||
|
array $context = []
|
||||||
|
): bool {
|
||||||
|
return array_key_exists('media_type', $context) &&
|
||||||
|
$context['media_type'] === MediaType::TvEpisode->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TmdbResult::class => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Framework\Serializer;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
class TmdbTvShowResultDenormalizer extends TmdbResultDenormalizer implements DenormalizerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire(service: 'serializer.normalizer.object')]
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
) {
|
||||||
|
parent::__construct($normalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): TmdbResult|array|null
|
||||||
|
{
|
||||||
|
/** @var TmdbResult $result */
|
||||||
|
$result = parent::denormalize($data, TmdbResult::class, $format, $context);
|
||||||
|
|
||||||
|
if (!in_array($data['first_air_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['first_air_date']));
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('seasons', $data)) {
|
||||||
|
$result->numberSeasons = count($data['seasons']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 = MediaType::TvShow->value;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization(
|
||||||
|
mixed $data,
|
||||||
|
string $type,
|
||||||
|
?string $format = null,
|
||||||
|
array $context = []
|
||||||
|
): bool {
|
||||||
|
return array_key_exists('media_type', $context) &&
|
||||||
|
$context['media_type'] === MediaType::TvShow->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TmdbResult::class => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Tmdb;
|
|||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Base\Enum\MediaType;
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Util\ImdbMatcher;
|
use App\Base\Util\ImdbMatcher;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
@@ -37,6 +38,16 @@ class TmdbClient
|
|||||||
protected TvEpisodeRepository $tvEpisodeRepository;
|
protected TvEpisodeRepository $tvEpisodeRepository;
|
||||||
protected SearchRepository $searchRepository;
|
protected SearchRepository $searchRepository;
|
||||||
|
|
||||||
|
protected array $mediaTypeMap = [
|
||||||
|
MediaType::Movie->value => MediaType::Movie->value,
|
||||||
|
MediaType::TvShow->value => MediaType::TvShow->value,
|
||||||
|
MediaType::TvEpisode->value => MediaType::TvEpisode->value,
|
||||||
|
'movie' => 'movies',
|
||||||
|
'tv' => 'tvshows',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $repos = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
@@ -96,6 +107,11 @@ 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->repos = [
|
||||||
|
MediaType::Movie->value => $this->movieRepository,
|
||||||
|
MediaType::TvShow->value => $this->tvRepository,
|
||||||
|
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function search(string $term): TmdbResult|Map
|
public function search(string $term): TmdbResult|Map
|
||||||
@@ -109,17 +125,15 @@ class TmdbClient
|
|||||||
$handler = $handlers[$data['media_type']];
|
$handler = $handlers[$data['media_type']];
|
||||||
return $this->$handler($term);
|
return $this->$handler($term);
|
||||||
}
|
}
|
||||||
return $this->parseListOfResults(
|
$results = $this->searchRepository->getApi()->searchMulti($term);
|
||||||
$this->searchRepository->getApi()->searchMulti($term),
|
return $this->parseListOfResults($results);
|
||||||
"movies"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function movieDetails(string $imdbId): ?TmdbResult
|
public function movieDetails(string $imdbId): ?TmdbResult
|
||||||
{
|
{
|
||||||
$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']),
|
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids,credits']),
|
||||||
MediaType::Movie->value,
|
MediaType::Movie->value,
|
||||||
$imdbId
|
$imdbId
|
||||||
);
|
);
|
||||||
@@ -128,14 +142,14 @@ 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']);
|
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids,credits']);
|
||||||
|
|
||||||
$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 &&
|
||||||
strtolower($data['name']) !== 'specials';
|
strtolower($data['name']) !== 'specials' &&
|
||||||
|
$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) {
|
|
||||||
return $data[1]['season_number'];
|
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
@@ -147,7 +161,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);
|
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => 'external_ids,credits']);
|
||||||
$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'];
|
||||||
@@ -156,9 +170,9 @@ class TmdbClient
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): ?TmdbResult
|
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
||||||
{
|
{
|
||||||
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids']);
|
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$result,
|
$result,
|
||||||
MediaType::TvEpisode->value,
|
MediaType::TvEpisode->value,
|
||||||
@@ -168,56 +182,46 @@ class TmdbClient
|
|||||||
|
|
||||||
public function relatedMedia(string $tmdbId, string $mediaType, int $resultCount = 6): Map
|
public function relatedMedia(string $tmdbId, string $mediaType, int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
$repos = [
|
$results = $this->repos[$mediaType]->getApi()->getRecommendations($tmdbId, ['append_to_response' => 'external_ids']);
|
||||||
'movies' => $this->movieRepository,
|
|
||||||
'tvshows' => $this->tvRepository,
|
|
||||||
];
|
|
||||||
$results = $repos[$mediaType]->getApi()->getRecommendations($tmdbId);
|
|
||||||
|
|
||||||
return $this->parseListOfResults(
|
return $this->parseListOfResults(
|
||||||
$results,
|
$results,
|
||||||
$mediaType,
|
|
||||||
$resultCount
|
$resultCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function popularMovies(int $resultCount = 6): Map
|
public function popularMovies(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
|
$results = $this->movieRepository->getApi()->getPopular();
|
||||||
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
|
$result['media_type'] = MediaType::Movie->value;
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
return $this->parseListOfResults(
|
return $this->parseListOfResults(
|
||||||
$this->movieRepository->getApi()->getPopular(),
|
$results,
|
||||||
"movies",
|
|
||||||
$resultCount
|
$resultCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function popularTvShows(int $resultCount = 6): Map
|
public function popularTvShows(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
|
$results = $this->tvRepository->getApi()->getPopular();
|
||||||
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
|
$result['media_type'] = MediaType::TvShow->value;
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
return $this->parseListOfResults(
|
return $this->parseListOfResults(
|
||||||
$this->tvRepository->getApi()->getPopular(),
|
$results,
|
||||||
"tvshows",
|
|
||||||
$resultCount
|
$resultCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getExternalIds(string $tmdbId, $mediaType): ?array
|
private function getExternalIds(int $tmdbId, string $mediaType): ?array
|
||||||
{
|
{
|
||||||
try {
|
if (!array_key_exists($mediaType, $this->repos) ||
|
||||||
switch (MediaType::tryFrom($mediaType)->value) {
|
!in_array($mediaType, [MediaType::Movie->value, MediaType::TvShow->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 [];
|
||||||
}
|
}
|
||||||
|
return $this->repos[$mediaType]->getApi()->getExternalIds($tmdbId);
|
||||||
return $externalIds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findByImdbId(string $imdbId): array
|
private function findByImdbId(string $imdbId): array
|
||||||
@@ -236,7 +240,7 @@ class TmdbClient
|
|||||||
throw new \Exception("No results found for $imdbId");
|
throw new \Exception("No results found for $imdbId");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseResult(array $data, string $mediaType, string $imdbId): TmdbResult
|
private function parseResult(array $data, string $mediaType, string $imdbId): TmdbResult|TmdbEpisodeDto
|
||||||
{
|
{
|
||||||
if (!array_key_exists('external_ids', $data)) {
|
if (!array_key_exists('external_ids', $data)) {
|
||||||
$data['external_ids'] = ['imdb_id' => $imdbId];
|
$data['external_ids'] = ['imdb_id' => $imdbId];
|
||||||
@@ -244,20 +248,21 @@ class TmdbClient
|
|||||||
return $this->serializer->denormalize($data, TmdbResult::class, context: ['media_type' => $mediaType]);
|
return $this->serializer->denormalize($data, TmdbResult::class, context: ['media_type' => $mediaType]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseListOfResults(array $data, string $mediaType, ?int $resultCount = null): Map
|
private function parseListOfResults(array $data, ?int $resultCount = null): Map
|
||||||
{
|
{
|
||||||
$results = Map::from($data['results'])->filter(
|
$results = Map::from($data['results'])->filter(function ($result) {
|
||||||
fn ($result) => array_key_exists('id', $result)
|
return array_key_exists('media_type', $result) &&
|
||||||
)->map(function ($result) {
|
in_array($result['media_type'], array_keys($this->mediaTypeMap));
|
||||||
$result['external_ids'] = $this->getExternalIds($result['id'], MediaType::Movie->value);
|
})->map(function ($result) {
|
||||||
|
$result['external_ids'] = $this->getExternalIds($result['id'], $this->mediaTypeMap[$result['media_type']]);
|
||||||
return $result;
|
return $result;
|
||||||
})->filter(function ($result) {
|
})->filter(function ($result) {
|
||||||
return array_key_exists('id', $result) &&
|
return array_key_exists('id', $result) &&
|
||||||
array_key_exists('imdb_id', $result['external_ids']) &&
|
array_key_exists('imdb_id', $result['external_ids']) &&
|
||||||
$result['external_ids']['imdb_id'] !== null &&
|
$result['external_ids']['imdb_id'] !== null &&
|
||||||
$result['external_ids']['imdb_id'] !== "";
|
$result['external_ids']['imdb_id'] !== "";
|
||||||
})->map(function ($result) use ($mediaType) {
|
})->map(function ($result) {
|
||||||
return $this->serializer->denormalize($result, TmdbResult::class, context: ['media_type' => $mediaType]);
|
return $this->serializer->denormalize($result, TmdbResult::class, context: ['media_type' => $this->mediaTypeMap[$result['media_type']]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (null !== $resultCount) {
|
if (null !== $resultCount) {
|
||||||
|
|||||||
@@ -2,10 +2,35 @@
|
|||||||
|
|
||||||
namespace App\Tmdb;
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\Dto\CastMemberDto;
|
||||||
|
use App\Tmdb\Dto\CrewMemberDto;
|
||||||
|
use App\Tmdb\Dto\GenreDto;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Context;
|
||||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
class TmdbResult
|
class TmdbResult
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param string|null $imdbId
|
||||||
|
* @param int|null $tmdbId
|
||||||
|
* @param string|null $title
|
||||||
|
* @param string|null $description
|
||||||
|
* @param string|null $poster
|
||||||
|
* @param \DateTimeInterface|null $premiereDate
|
||||||
|
* @param string|null $year
|
||||||
|
* @param string|null $mediaType
|
||||||
|
* @param array<TmdbEpisodeDto[]>|null $episodes
|
||||||
|
* @param string|null $episodeAirDate
|
||||||
|
* @param GenreDto[]|null $genres
|
||||||
|
* @param CastMemberDto[]|null $stars
|
||||||
|
* @param CrewMemberDto[]|null $directors
|
||||||
|
* @param CrewMemberDto[]|null $creators
|
||||||
|
* @param CrewMemberDto[]|null $producers
|
||||||
|
* @param int|null $runtime
|
||||||
|
* @param int|null $numberSeasons
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[SerializedPath('[external_ids][imdb_id]')]
|
#[SerializedPath('[external_ids][imdb_id]')]
|
||||||
public ?string $imdbId = "",
|
public ?string $imdbId = "",
|
||||||
@@ -18,8 +43,18 @@ class TmdbResult
|
|||||||
public ?\DateTimeInterface $premiereDate = null,
|
public ?\DateTimeInterface $premiereDate = null,
|
||||||
public ?string $year = null,
|
public ?string $year = null,
|
||||||
public ?string $mediaType = "",
|
public ?string $mediaType = "",
|
||||||
|
#[Context(denormalizationContext: [
|
||||||
|
'media_type' => MediaType::TvEpisode->value
|
||||||
|
])]
|
||||||
#[SerializedPath('[seasons]')]
|
#[SerializedPath('[seasons]')]
|
||||||
public ?array $episodes = null,
|
public ?array $episodes = null,
|
||||||
public ?string $episodeAirDate = null,
|
public ?string $episodeAirDate = null,
|
||||||
|
public ?array $genres = null,
|
||||||
|
public ?array $stars = null,
|
||||||
|
public ?array $directors = null,
|
||||||
|
public ?array $creators = null,
|
||||||
|
public ?array $producers = null,
|
||||||
|
public ?int $runtime = null,
|
||||||
|
public ?int $numberSeasons = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
<?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,14 +3,15 @@
|
|||||||
namespace App\Torrentio\Action\Result;
|
namespace App\Torrentio\Action\Result;
|
||||||
|
|
||||||
use App\Library\Dto\MediaFileDto;
|
use App\Library\Dto\MediaFileDto;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
class GetTvShowOptionsResult implements ResultInterface
|
class GetTvShowOptionsResult implements ResultInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $parentShow,
|
public TmdbResult|TmdbEpisodeDto $parentShow,
|
||||||
public TmdbResult $media,
|
public TmdbResult|TmdbEpisodeDto $media,
|
||||||
public MediaFileDto|false $file,
|
public MediaFileDto|false $file,
|
||||||
public string $season,
|
public string $season,
|
||||||
public string $episode,
|
public string $episode,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Torrentio
|
|||||||
$results = $this->client->get($imdbCode, $cacheTags);
|
$results = $this->client->get($imdbCode, $cacheTags);
|
||||||
|
|
||||||
if (true === $parseResults) {
|
if (true === $parseResults) {
|
||||||
return $this->parse($results);
|
return $this->parse($results, $imdbCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
@@ -33,13 +33,13 @@ class Torrentio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (true === $parseResults) {
|
if (true === $parseResults) {
|
||||||
return $this->parse($results);
|
return $this->parse($results, $imdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parse(array $data): array
|
public function parse(array $data, string $imdbId): array
|
||||||
{
|
{
|
||||||
$results = [];
|
$results = [];
|
||||||
foreach ($data['streams'] as $stream) {
|
foreach ($data['streams'] as $stream) {
|
||||||
@@ -59,7 +59,8 @@ class Torrentio
|
|||||||
$result = ResultFactory::map(
|
$result = ResultFactory::map(
|
||||||
$stream['url'],
|
$stream['url'],
|
||||||
$stream['title'],
|
$stream['title'],
|
||||||
$bingeGroup
|
$bingeGroup,
|
||||||
|
$imdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
$results[] = $result;
|
$results[] = $result;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class ResultFactory
|
|||||||
public static function map(
|
public static function map(
|
||||||
string $url,
|
string $url,
|
||||||
string $title,
|
string $title,
|
||||||
string $bingeGroup = "-"
|
string $bingeGroup = "-",
|
||||||
|
string $imdbId = "-",
|
||||||
) {
|
) {
|
||||||
$ptn = (object) (new PTN())->parse($title);
|
$ptn = (object) (new PTN())->parse($title);
|
||||||
return new TorrentioResult(
|
return new TorrentioResult(
|
||||||
@@ -40,7 +41,8 @@ class ResultFactory
|
|||||||
self::setLanguages($title),
|
self::setLanguages($title),
|
||||||
self::setLanguageFlags($title),
|
self::setLanguageFlags($title),
|
||||||
false,
|
false,
|
||||||
uniqid()
|
uniqid(),
|
||||||
|
$imdbId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class TorrentioResult
|
|||||||
public ?array $languages = [],
|
public ?array $languages = [],
|
||||||
public ?string $languageFlags = "-",
|
public ?string $languageFlags = "-",
|
||||||
public ?bool $selected = false,
|
public ?bool $selected = false,
|
||||||
public ?string $localId = "-"
|
public ?string $localId = "-",
|
||||||
|
public ?string $imdbId = "-",
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Twig\Extensions;
|
namespace App\Twig\Extensions;
|
||||||
|
|
||||||
|
use App\Base\ConfigResolver;
|
||||||
|
use App\Base\Dto\AppVersionDto;
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
use App\Twig\Dto\EpisodeIdDto;
|
use App\Twig\Dto\EpisodeIdDto;
|
||||||
use ChrisUllyott\FileSize;
|
use ChrisUllyott\FileSize;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Twig\Attribute\AsTwigFilter;
|
use Twig\Attribute\AsTwigFilter;
|
||||||
use Twig\Attribute\AsTwigFunction;
|
use Twig\Attribute\AsTwigFunction;
|
||||||
|
|
||||||
@@ -13,9 +16,16 @@ class UtilExtension
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly ConfigResolver $config,
|
||||||
private readonly MediaFiles $mediaFiles,
|
private readonly MediaFiles $mediaFiles,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
#[AsTwigFunction('app_version')]
|
||||||
|
public function app_version(): AppVersionDto
|
||||||
|
{
|
||||||
|
return $this->config->getAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
#[AsTwigFunction('uniqid')]
|
#[AsTwigFunction('uniqid')]
|
||||||
public function uniqid(): string
|
public function uniqid(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
<div class="mt-2 inline-flex gap-4 justify-between text-white">
|
<div class="mt-2 inline-flex gap-4 justify-between text-white">
|
||||||
<a class="text-sm" href="{{ path('app_login') }}">Sign In</a>
|
<a class="text-sm" href="{{ path('app_login') }}">Sign In</a>
|
||||||
<span class="text-sm">v{{ version }}</span>
|
<span class="text-sm">{{ app_version() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<p class="px-4 pt-1 inline-flex justify-center">
|
<p class="px-4 pt-1 inline-flex justify-center">
|
||||||
<small class="text-white text-xs">v{{ version|default('0.0') }}</small>
|
<small class="text-white text-xs">{{ app_version() }}</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
>
|
>
|
||||||
<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">
|
||||||
{% for episode in this.getEpisodes().items %}
|
{% for episode in this.getEpisodes().items %}
|
||||||
<episode-container id="{{ episode_anchor(episode['season_number'], episode['episode_number']) }}" class="results"
|
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
|
||||||
show-title="{{ this.title }}"
|
show-title="{{ this.title }}"
|
||||||
data-tv-results-loading-icon-outlet=".loading-icon"
|
data-tv-results-loading-icon-outlet=".loading-icon"
|
||||||
data-download-button-outlet=".download-btn"
|
data-download-button-outlet=".download-btn"
|
||||||
@@ -11,15 +11,15 @@
|
|||||||
title: this.title,
|
title: this.title,
|
||||||
tmdbId: this.tmdbId,
|
tmdbId: this.tmdbId,
|
||||||
imdbId: this.imdbId,
|
imdbId: this.imdbId,
|
||||||
season: episode['season_number'],
|
season: episode.seasonNumber,
|
||||||
episode: episode['episode_number'],
|
episode: episode.episodeNumber,
|
||||||
active: 'true',
|
active: 'true',
|
||||||
}) }}
|
}) }}
|
||||||
>
|
>
|
||||||
<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 episode.poster != null %}
|
||||||
<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">
|
||||||
<twig:ux:icon width="32" name="hugeicons:loading-01" />
|
<twig:ux:icon width="32" name="hugeicons:loading-01" />
|
||||||
@@ -27,28 +27,28 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex flex-col gap-4 grow">
|
<div class="flex flex-col gap-4 grow">
|
||||||
<h4 class="text-md font-bold">
|
<h4 class="text-md font-bold">
|
||||||
{{ episode['episode_number'] }}. {{ episode['name'] }}
|
{{ episode.episodeNumber }}. {{ episode.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p>{{ episode['overview']|truncate }}</p>
|
<p>{{ episode.description|truncate }}</p>
|
||||||
<div>
|
<div class="text-xs font-bold">
|
||||||
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode['season_number'] }} episode {{ episode['episode_number'] }}.">
|
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
|
||||||
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title='"{{ episode['name'] }}" aired on {{ episode['air_date']|date(null, 'UTC') }}.'>
|
<span class="py-1 px-1.5 mr-1 grow-0 bg-gray-700 rounded-lg text-white" title='"{{ episode.name }}" aired on {{ episode.airDate|date(null, 'UTC') }}.'>
|
||||||
{{ episode['air_date']|date(null, 'UTC') }}
|
{{ episode.airDate|date(null, 'UTC') }}
|
||||||
</small>
|
</span>
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('api.library.search', {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
season: episode['season_number'],
|
season: episode.seasonNumber,
|
||||||
episode: episode['episode_number'],
|
episode: episode.episodeNumber,
|
||||||
block: 'media_exists_badge',
|
block: 'media_exists_badge',
|
||||||
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode.seasonNumber, episode.episodeNumber)
|
||||||
}) }}">
|
}) }}">
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
<span class="py-1 px-1.5 mr-1 grow-0 bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||||
missing
|
missing
|
||||||
</small>
|
</span>
|
||||||
</twig:Turbo:Frame>
|
</twig:Turbo:Frame>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="dropdown-button flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90" title="Click to expand the results table for season {{ episode['season_number'] }} episode {{ episode['episode_number'] }}.">
|
<button class="dropdown-button flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
|
||||||
<path
|
<path
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
||||||
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
|
<twig:Turbo:Frame id="results_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('app_torrentio_tvshows', {
|
||||||
tmdbId: this.tmdbId,
|
tmdbId: this.tmdbId,
|
||||||
imdbId: this.imdbId,
|
imdbId: this.imdbId,
|
||||||
season: episode['season_number'],
|
season: episode.seasonNumber,
|
||||||
episode: episode['episode_number'],
|
episode: episode.episodeNumber,
|
||||||
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
|
target: 'results_' ~ episode_id(episode.seasonNumber, episode.episodeNumber),
|
||||||
block: 'tvshow_results'
|
block: 'tvshow_results'
|
||||||
}) }}" />
|
}) }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,20 +12,20 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<small
|
<span
|
||||||
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||||
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||||
>
|
>
|
||||||
exists
|
exists
|
||||||
</small>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if result.exists == false %}
|
{% if result.exists == false %}
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white"
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white"
|
||||||
title="Media has not been downloaded yet.">
|
title="Media has not been downloaded yet.">
|
||||||
missing
|
missing
|
||||||
</small>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</template>
|
</template>
|
||||||
</turbo-stream>
|
</turbo-stream>
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<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">
|
||||||
{% if results.media.poster != null %}
|
{% if results.media.poster != null %}
|
||||||
<img class="w-full md:w-40 rounded-lg" src="{{ results.media.poster }}" />
|
<img class="w-full md:w-[12.5rem] rounded-lg" src="{{ results.media.poster }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full md:w-40 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
<div class="w-full md:w-[12.5rem] h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
<twig:ux:icon width="24" name="hugeicons:loading-01" />
|
<twig:ux:icon width="24" name="hugeicons:loading-01" />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -20,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|date('Y') }})
|
{{ results.media.title }} ({{ results.media.year }})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
@@ -51,29 +51,70 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-gray-50">
|
<p class="text-gray-50 mb-4">
|
||||||
{{ results.media.description }}
|
{{ results.media.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if results.media.stars != null %}
|
||||||
|
<strong>Starring</strong>: {{ results.media.stars|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.directors != null %}
|
||||||
|
<strong>Directors</strong>: {{ results.media.directors|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.producers != null %}
|
||||||
|
<strong>Producers</strong>: {{ results.media.producers|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.creators != null %}
|
||||||
|
<strong>Creators</strong>: {{ results.media.creators|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.genres != null %}
|
||||||
|
<div id="genres" class="text-gray-50 my-4">
|
||||||
|
{% for genre in results.media.genres %}
|
||||||
|
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
|
<span>{{ results.media.numberSeasons }}</span> season(s)
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
|
||||||
|
{{ results.media.premiereDate|date(null, 'UTC') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if "movies" == results.media.mediaType %}
|
{% if "movies" == results.media.mediaType %}
|
||||||
<div class="flex flex-row justify-start items-end grow">
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
<span class="results-count-number" id="movie_results_count">-</span> results
|
<span class="results-count-number" id="movie_results_count">-</span> results
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Release date {{ results.media.episodeAirDate }}">
|
|
||||||
{{ results.media.episodeAirDate|date(null, 'UTC') }}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||||
title: results.media.title,
|
title: results.media.title,
|
||||||
block: 'media_exists_badge',
|
block: 'media_exists_badge',
|
||||||
target: "meb_" ~ results.media.imdbId
|
target: "meb_" ~ results.media.imdbId
|
||||||
}) }}">
|
}) }}">
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
||||||
missing
|
missing
|
||||||
</small>
|
</span>
|
||||||
</twig:Turbo:Frame>
|
</twig:Turbo:Frame>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||||
|
{{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white" title="This movie has a runtime of {{ results.media.runtime }} minutes.">
|
||||||
|
{{ results.media.runtime }} minutes
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
provider="{{ result.provider }}"
|
provider="{{ result.provider }}"
|
||||||
languages="{{ result.languages|json_encode }}"
|
languages="{{ result.languages|json_encode }}"
|
||||||
media-type="{{ results.media.mediaType }}"
|
media-type="{{ results.media.mediaType }}"
|
||||||
imdb-id="{{ results.media.imdbId }}"
|
imdb-id="{{ result.imdbId }}"
|
||||||
filename="{{ result.filename }}"
|
filename="{{ result.filename }}"
|
||||||
data-local-id="{{ result.localId }}"
|
data-local-id="{{ result.localId }}"
|
||||||
{% if "tvshows" == results.media.mediaType %}
|
{% if "tvshows" == results.media.mediaType %}
|
||||||
|
|||||||
Reference in New Issue
Block a user