Compare commits

...

19 Commits

Author SHA1 Message Date
ca89eff236 fix: adds .dockerignore, includes .git in deployment.properties 2025-09-15 14:55:32 -05:00
53da7a746b fix: adds app version back to worker & scheduler images 2025-09-15 14:26:15 -05:00
981699bc13 chore: creates base worker image 2025-09-15 13:15:10 -05:00
52f460ff62 fix: missing imdb id on torrentio results 2025-09-15 12:34:04 -05:00
a42e0d4d1a fix: handles internal app version better 2025-09-15 12:33:16 -05:00
3e4a2d9bb1 fix: references correct base image tag 2025-09-15 10:06:35 -05:00
af8a30826c fix: monitors after tmdb updates 2025-09-15 09:41:20 -05:00
09e1c75826 fix: replaces test swarm file with dynamic node port 2025-09-14 23:00:40 -05:00
f1b8b34359 fix: adds deploy.test.compose.yml 2025-09-14 22:53:06 -05:00
6f9db68664 fix: installs wget into worker image 2025-09-14 22:11:58 -05:00
aeb706b5af build: creates base image to speed up build times 2025-09-14 22:10:04 -05:00
7918c260e5 fix: uses base docker image 2025-09-09 16:30:22 -05:00
38130ea0ec fix: typo 2025-09-09 12:11:40 -05:00
da403958dc fix: docker reference 2025-09-09 12:04:14 -05:00
c2bafabb20 fix: builds worker & scheduler FROM app 2025-09-09 11:48:43 -05:00
e6983aedf9 Merge branch 'dev-tmdb-cleanup' 2025-09-09 11:14:02 -05:00
ee076518b3 fix: style 2025-09-08 21:28:01 -05:00
a2f16398be fix: composer.lock 2025-09-08 20:22:31 -05:00
0f03199eb4 fix: cascade removes monitors 2025-09-08 16:05:31 -05:00
25 changed files with 206 additions and 62 deletions

14
.dockerignore Normal file
View 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
View 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

57
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c133ccd27ac6a41256bdc69129c16546",
"content-hash": "e055bbbbe5836c92bb147b6dbb1d1d46",
"packages": [
{
"name": "1tomany/rich-bundle",
@@ -9467,6 +9467,57 @@
],
"time": "2025-05-12T14:48:23+00:00"
},
{
"name": "symfony/serializer-pack",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer-pack.git",
"reference": "2844d81a5fc86b617b82f44a8bfcaaba1d583eee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer-pack/zipball/2844d81a5fc86b617b82f44a8bfcaaba1d583eee",
"reference": "2844d81a5fc86b617b82f44a8bfcaaba1d583eee",
"shasum": ""
},
"require": {
"phpdocumentor/reflection-docblock": "*",
"phpstan/phpdoc-parser": "*",
"symfony/property-access": "*",
"symfony/property-info": "*",
"symfony/serializer": "*"
},
"conflict": {
"symfony/property-info": "<5.4",
"symfony/serializer": "<5.4"
},
"type": "symfony-pack",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A pack for the Symfony serializer",
"support": {
"issues": "https://github.com/symfony/serializer-pack/issues",
"source": "https://github.com/symfony/serializer-pack/tree/v1.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-06-03T13:55:25+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.6.0",
@@ -13347,7 +13398,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -13355,7 +13406,7 @@
"ext-ctype": "*",
"ext-iconv": "*"
},
"platform-dev": [],
"platform-dev": {},
"platform-overrides": {
"php": "8.4"
},

View File

@@ -32,7 +32,7 @@ parameters:
app.cache.redis.host.default: 'redis://redis'
# Various configs
app.default.version: '0.dev'
app.default.version: '0.0.0-dev'
app.default.timezone: 'America/Chicago'
# Auth

View File

@@ -2,7 +2,7 @@ services:
app:
image: registry.caldwell.digital/home/torsearch-app:${TAG}
ports:
- '8001:80'
- "${SWARM_PORT}:80"
environment:
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'

View File

@@ -10,6 +10,7 @@ templates
var
vendor
build.xml
.git
.env
.env.local
composer.json

View File

@@ -1,19 +1,8 @@
FROM dunglas/frankenphp
FROM code.caldwell.digital/home/torsearch-base:php8.4
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ARG APP_VERSION="0.dev"
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
COPY . /app
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile

View 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

View 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

View File

@@ -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" ]

View File

@@ -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"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ARG APP_VERSION="0.dev"
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
RUN apk add --no-cache wget
COPY . /app
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]

View File

@@ -2,16 +2,25 @@
namespace App\Base;
use App\Base\Dto\AppVersionDto;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
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 = [];
public function __construct(
private readonly DenormalizerInterface $denormalizer,
#[Autowire(param: 'app.url')]
private readonly ?string $appUrl = null,
#[Autowire(param: 'app.version')]
private readonly ?string $appVersion = null,
#[Autowire(param: 'app.debrid.real_debrid.key')]
private readonly ?string $realDebridApiKey = null,
@@ -92,6 +101,13 @@ final class ConfigResolver
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
{
return [

View 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 : '');
}
}

View File

@@ -2,12 +2,14 @@
namespace App\Base\Framework\Controller;
use App\Monitor\Action\Command\MonitorTvShowCommand;
use App\Monitor\Action\Handler\MonitorTvShowHandler;
use App\Tmdb\TmdbClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Attribute\Route;
@@ -46,8 +48,9 @@ final class IndexController extends AbstractController
}
#[Route('/test')]
public function monitorTvShow(): Response
public function monitorTvShow(MonitorTvShowHandler $handler): Response
{
// $handler->handle(new MonitorTvShowCommand(82));
return $this->render('index/test.html.twig', []);
}
}

View File

@@ -42,7 +42,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
$monitor = $this->monitorRepository->find($command->movieMonitorId);
$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) {
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));

View File

@@ -9,6 +9,7 @@ use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
use App\Monitor\Action\Result\MonitorTvShowResult;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Tmdb\Dto\TmdbEpisodeDto;
use App\Tmdb\TmdbClient;
use Carbon\Carbon;
use DateTimeImmutable;
@@ -61,8 +62,9 @@ readonly class MonitorTvShowHandler implements HandlerInterface
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
// Dispatch Episode commands for each missing Episode
foreach ($episodesInShow as $episode) {
/** @var TmdbEpisodeDto $episode */
// 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);
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
if (false === $episodeInFuture) {
@@ -94,9 +96,9 @@ readonly class MonitorTvShowHandler implements HandlerInterface
->setImdbId($monitor->getImdbId())
->setTitle($monitor->getTitle())
->setMonitorType('tvepisode')
->setSeason($episode['season_number'])
->setEpisode($episode['episode_number'])
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
->setSeason($episode->seasonNumber)
->setEpisode($episode->episodeNumber)
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
->setCreatedAt(new DateTimeImmutable())
->setSearchCount(0)
->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);
$episodeAirDate = Carbon::parse($episodeInShow['air_date']);
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
return $episodeAirDate >= $monitorStartDate;
}
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
{
return $downloadedEpisodes->filter(
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
&& $episode->season === $episodeInShow['season_number']
fn (object $episode) => $episode->episode === $episodeInShow->episodeNumber
&& $episode->season === $episodeInShow->seasonNumber
)->count() > 0;
}
private function monitorExists(Monitor $monitor, array $episode): bool
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
{
return $this->monitorRepository->findOneBy([
'imdbId' => $monitor->getImdbId(),
'title' => $monitor->getTitle(),
'monitorType' => 'tvepisode',
'season' => $episode['season_number'],
'episode' => $episode['episode_number'],
'season' => $episode->seasonNumber,
'episode' => $episode->episodeNumber,
'status' => ['New', 'Active', 'In Progress']
]) !== null;
}

View File

@@ -68,7 +68,7 @@ class Monitor
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
private ?self $parent = null;
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent', cascade: ['remove'])]
private Collection $children;
public function __construct()

View File

@@ -17,7 +17,7 @@ class Torrentio
$results = $this->client->get($imdbCode, $cacheTags);
if (true === $parseResults) {
return $this->parse($results);
return $this->parse($results, $imdbCode);
}
return $results;
@@ -33,13 +33,13 @@ class Torrentio
}
if (true === $parseResults) {
return $this->parse($results);
return $this->parse($results, $imdbId);
}
return $results;
}
public function parse(array $data): array
public function parse(array $data, string $imdbId): array
{
$results = [];
foreach ($data['streams'] as $stream) {
@@ -59,7 +59,8 @@ class Torrentio
$result = ResultFactory::map(
$stream['url'],
$stream['title'],
$bingeGroup
$bingeGroup,
$imdbId
);
$results[] = $result;

View File

@@ -18,7 +18,8 @@ class ResultFactory
public static function map(
string $url,
string $title,
string $bingeGroup = "-"
string $bingeGroup = "-",
string $imdbId = "-",
) {
$ptn = (object) (new PTN())->parse($title);
return new TorrentioResult(
@@ -40,7 +41,8 @@ class ResultFactory
self::setLanguages($title),
self::setLanguageFlags($title),
false,
uniqid()
uniqid(),
$imdbId,
);
}

View File

@@ -23,6 +23,7 @@ class TorrentioResult
public ?array $languages = [],
public ?string $languageFlags = "-",
public ?bool $selected = false,
public ?string $localId = "-"
public ?string $localId = "-",
public ?string $imdbId = "-",
) {}
}

View File

@@ -2,10 +2,13 @@
namespace App\Twig\Extensions;
use App\Base\ConfigResolver;
use App\Base\Dto\AppVersionDto;
use App\Base\Service\MediaFiles;
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
use App\Twig\Dto\EpisodeIdDto;
use ChrisUllyott\FileSize;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
@@ -13,9 +16,16 @@ class UtilExtension
{
public function __construct(
private readonly ConfigResolver $config,
private readonly MediaFiles $mediaFiles,
) {}
#[AsTwigFunction('app_version')]
public function app_version(): AppVersionDto
{
return $this->config->getAppVersion();
}
#[AsTwigFunction('uniqid')]
public function uniqid(): string
{

View File

@@ -20,7 +20,7 @@
{% block body %}{% endblock %}
<div class="mt-2 inline-flex gap-4 justify-between text-white">
<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>
</body>

View File

@@ -55,7 +55,7 @@
</div>
</a>
<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>
</div>
</nav>

View File

@@ -10,9 +10,9 @@
<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">
{% 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 %}
<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" />
</div>
{% endif %}

View File

@@ -52,7 +52,7 @@
provider="{{ result.provider }}"
languages="{{ result.languages|json_encode }}"
media-type="{{ results.media.mediaType }}"
imdb-id="{{ results.media.imdbId }}"
imdb-id="{{ result.imdbId }}"
filename="{{ result.filename }}"
data-local-id="{{ result.localId }}"
{% if "tvshows" == results.media.mediaType %}