Compare commits

..

17 Commits

Author SHA1 Message Date
Brock H Caldwell
4087543e78 fix: monitors check for episodes in folders w/ & w/o the year appended (backwards compatible) 2026-03-18 09:29:05 -05:00
Brock H Caldwell
79d9d61592 fix: includes year in media directory name 2026-03-17 23:16:11 -05:00
Brock H Caldwell
4d0a198510 fix(ci): moves build_base_image to workflow_dispatch 2026-03-15 19:58:45 -05:00
Brock H Caldwell
5da1dde24d chore: adds enum for monitor types
Some checks failed
CI / build-base-app (push) Failing after 43s
CI / build-base-worker-supervisord (push) Successful in 43s
2026-03-15 19:57:00 -05:00
Brock H Caldwell
6ed327f78e task(ci): update base image
Some checks failed
CI / build-base-worker-supervisord (push) Failing after 37s
CI / build-base-app (push) Failing after 38s
2026-03-11 08:37:33 -05:00
Brock H Caldwell
1827c1df71 task(ci): update base image
Some checks failed
CI / build-base-app (push) Failing after 30s
CI / build-base-worker (push) Successful in 34s
CI / build-base-worker-supervisord (push) Successful in 2m1s
2026-03-11 08:26:08 -05:00
Brock H Caldwell
0c5fd2544b task(ci): update base image
Some checks failed
CI / build-base-worker (push) Successful in 35s
CI / build-base-app (push) Successful in 35s
CI / build-base-worker-supervisord (push) Failing after 37s
2026-03-11 08:22:03 -05:00
Brock H Caldwell
a6d5e2b026 task(ci): update base image 2026-03-10 23:58:47 -05:00
Brock H Caldwell
688ea98922 task(ci): update base image 2026-03-10 23:57:11 -05:00
Brock H Caldwell
d55a9cfdd5 task(ci): update base image 2026-03-10 23:55:52 -05:00
Brock H Caldwell
e9021c22fa task(ci): update base image
Some checks failed
CI / build-base-worker (push) Successful in 53s
CI / build-base-worker-supervisord (push) Failing after 2m4s
CI / build-base-app (push) Successful in 2m19s
2026-03-10 23:51:32 -05:00
Brock H Caldwell
939660a715 task(ci): build base images
Some checks failed
CI / build-base-worker (push) Failing after 32s
CI / build-base-app (push) Successful in 40s
CI / build-base-worker-supervisord (push) Failing after 2m19s
2026-03-10 23:48:12 -05:00
Brock H Caldwell
fb3e7b20ff task(ci): build base images
Some checks failed
CI / build-base-worker (push) Failing after 35s
CI / build-base-worker-supervisord (push) Successful in 50s
CI / build-base-app (push) Successful in 3m1s
2026-03-10 23:09:20 -05:00
Brock H Caldwell
699dbaabc3 fix: build script, installs php exts to worker image
All checks were successful
CI / build-test (push) Successful in 3m8s
2026-03-10 20:22:53 -05:00
Brock H Caldwell
484ac40d99 fix: broken mercure 2026-03-10 20:21:27 -05:00
Brock H Caldwell
b8a22e63c9 WIP: CI docker
All checks were successful
CI / build-test (push) Successful in 3m15s
2026-03-09 15:20:41 -05:00
Brock H Caldwell
e489f73f7c fix(ci): adds --ignore-platform-reqs to composer
All checks were successful
CI / build-test (push) Successful in 2m54s
2026-03-09 13:59:25 -05:00
10 changed files with 148 additions and 36 deletions

2
.env
View File

@@ -31,6 +31,8 @@ DATABASE_URL=
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
MERCURE_PUBLISHER_JWT_KEY="!ChangeThisMercureHubJWTSecretKey!"
MERCURE_SUBSCRIBER_JWT_KEY="!ChangeThisMercureHubJWTSecretKey!"
###> symfony/messenger ### ###> symfony/messenger ###
# Choose one of the transports below # Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages

View File

@@ -0,0 +1,59 @@
name: CI
on:
workflow_dispatch:
jobs:
build-base-app:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Login to registry
uses: docker/login-action@v2
with:
registry: "${{ vars.REGISTRY_URL }}"
username: "${{ vars.REGISTRY_USER }}"
password: "${{ vars.REGISTRY_PASS }}"
- name: Build and push
uses: docker/build-push-action@v5
env:
APP_FRANKENPHP_TAG: php8.4
with:
context: .
push: true
file: docker/Dockerfile.base.worker
platforms: linux/amd64
build-args: |
APP_FRANKENPHP_TAG=${{ env.APP_FRANKENPHP_TAG }}
tags: |
code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
code.caldwell.digital/home/torsearch-base:latest
build-base-worker-supervisord:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Login to registry
uses: docker/login-action@v2
with:
registry: "${{ vars.REGISTRY_URL }}"
username: "${{ vars.REGISTRY_USER }}"
password: "${{ vars.REGISTRY_PASS }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
file: docker/Dockerfile.base.worker
platforms: linux/amd64
build-args: |
ALPINE_VERSION=3.22
tags: |
code.caldwell.digital/home/torsearch-base-worker-supervisord:latest

View File

@@ -22,7 +22,7 @@ jobs:
run: composer global require phing/phing run: composer global require phing/phing
- name: Run composer - name: Run composer
run: composer install --no-dev --no-scripts -o run: composer install --no-dev --no-scripts --ignore-platform-reqs -o
- name: Build tailwind - name: Build tailwind
run: APP_ENV=build php bin/console tailwind:build run: APP_ENV=build php bin/console tailwind:build
@@ -45,7 +45,7 @@ jobs:
tag="${{ gitea.REF_NAME }}" tag="${{ gitea.REF_NAME }}"
tmdb_api="${{ vars.TMDB_API }}" tmdb_api="${{ vars.TMDB_API }}"
version=${tag:1} version=${tag:1}
docker build -f docker/Dockerfile.app \ docker build --pull -f docker/Dockerfile.app \
-t code.caldwell.digital/home/torsearch-app:${version} \ -t code.caldwell.digital/home/torsearch-app:${version} \
-t code.caldwell.digital/home/torsearch-app:latest \ -t code.caldwell.digital/home/torsearch-app:latest \
--build-arg "APP_VERSION=${version}" \ --build-arg "APP_VERSION=${version}" \
@@ -59,7 +59,7 @@ jobs:
tag="${{ gitea.REF_NAME }}" tag="${{ gitea.REF_NAME }}"
tmdb_api="${{ vars.TMDB_API }}" tmdb_api="${{ vars.TMDB_API }}"
version=${tag:1} version=${tag:1}
docker build -f docker/Dockerfile.worker \ docker build --pull -f docker/Dockerfile.worker \
-t code.caldwell.digital/home/torsearch-worker:${version} \ -t code.caldwell.digital/home/torsearch-worker:${version} \
-t code.caldwell.digital/home/torsearch-worker:latest \ -t code.caldwell.digital/home/torsearch-worker:latest \
--build-arg "APP_VERSION=${version}" \ --build-arg "APP_VERSION=${version}" \
@@ -73,7 +73,7 @@ jobs:
tag="${{ gitea.REF_NAME }}" tag="${{ gitea.REF_NAME }}"
tmdb_api="${{ vars.TMDB_API }}" tmdb_api="${{ vars.TMDB_API }}"
version=${tag:1} version=${tag:1}
docker build -f docker/Dockerfile.scheduler \ docker build --pull -f docker/Dockerfile.scheduler \
-t code.caldwell.digital/home/torsearch-scheduler:${version} \ -t code.caldwell.digital/home/torsearch-scheduler:${version} \
-t code.caldwell.digital/home/torsearch-scheduler:latest \ -t code.caldwell.digital/home/torsearch-scheduler:latest \
--build-arg "APP_VERSION=${version}" \ --build-arg "APP_VERSION=${version}" \

View File

@@ -1,22 +1,19 @@
# torsearch-app is built from this base # torsearch-app is built from this base
export APP_FRANKENPHP_TAG=php8.4 export APP_FRANKENPHP_TAG=php8.4
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" . docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
docker build -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" . docker push --platform=linux/amd64 code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} docker push --platform=linux/amd64 code.caldwell.digital/home/torsearch-base:latest
docker push code.caldwell.digital/home/torsearch-base:latest
# torsearch-worker & torsearch-scheduler are built from this base # torsearch-worker & torsearch-scheduler are built from this base
export WORKER_FRANKENPHP_TAG=php8.4-alpine export WORKER_FRANKENPHP_TAG=php8.4-alpine
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" . docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
docker build -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" . docker push --platform=linux/amd64 code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} docker push --platform=linux/amd64 code.caldwell.digital/home/torsearch-base-worker:latest
docker push code.caldwell.digital/home/torsearch-base-worker:latest
# torsearch-worker-supervisord # torsearch-worker-supervisord
export ALPINE_VERSION=3.22 export ALPINE_VERSION=3.22
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" . docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" .
docker build -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" . docker push --platform=linux/amd64 code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
docker push code.caldwell.digital/home/torsearch-base-worker-supervisord:latest

View File

@@ -27,8 +27,8 @@ services:
tty: true tty: true
environment: environment:
TZ: America/Chicago TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!' MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}"
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}"
depends_on: depends_on:
database: database:
condition: service_healthy condition: service_healthy

View File

@@ -16,11 +16,13 @@ RUN apk add --no-cache \
php84-fileinfo \ php84-fileinfo \
php84-fpm \ php84-fpm \
php84-gd \ php84-gd \
php84-intl \
php84-mbstring \ php84-mbstring \
php84-mysqli \ php84-mysqli \
php84-opcache \ php84-opcache \
php84-openssl \ php84-openssl \
php84-pdo_mysql \ php84-pdo_mysql \
php84-session \
php84-tokenizer \ php84-tokenizer \
php84-xml \ php84-xml \
supervisor supervisor

View File

@@ -113,9 +113,14 @@ class MediaFiles
return Map::from($results); return Map::from($results);
} }
public function createMovieDirectory(string $path): string public function createMovieDirectory(string $title, string|int $year): string
{ {
$path = $this->moviesPath . DIRECTORY_SEPARATOR . $path; $path = sprintf(
'%s' . DIRECTORY_SEPARATOR . '%s (%s)',
$this->moviesPath,
$title,
$year
);
if (false === $this->filesystem->exists($path)) { if (false === $this->filesystem->exists($path)) {
$this->filesystem->mkdir($path); $this->filesystem->mkdir($path);
@@ -124,9 +129,14 @@ class MediaFiles
return $path; return $path;
} }
public function createTvShowDirectory(string $path): string public function createTvShowDirectory(string $title, string|int $year): string
{ {
$path = $this->tvShowsPath . DIRECTORY_SEPARATOR . $path; $path = sprintf(
'%s' . DIRECTORY_SEPARATOR . '%s (%s)',
$this->tvShowsPath,
$title,
$year
);
if (false === $this->filesystem->exists($path)) { if (false === $this->filesystem->exists($path)) {
$this->filesystem->mkdir($path); $this->filesystem->mkdir($path);

View File

@@ -7,6 +7,8 @@ use App\Base\Service\MediaFiles;
use App\Download\DownloadEvents; use App\Download\DownloadEvents;
use App\Download\Framework\Entity\Download; use App\Download\Framework\Entity\Download;
use App\EventLog\Action\Command\AddEventLogCommand; use App\EventLog\Action\Command\AddEventLogCommand;
use App\Search\Action\Command\GetMediaInfoCommand;
use App\Search\Action\Handler\GetMediaInfoHandler;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
@@ -25,6 +27,7 @@ class ProcessDownloader implements DownloaderInterface
private MediaFiles $mediaFiles, private MediaFiles $mediaFiles,
private CacheInterface $cache, private CacheInterface $cache,
private readonly Broadcaster $broadcaster, private readonly Broadcaster $broadcaster,
private readonly GetMediaInfoHandler $getMediaInfoHandler,
) {} ) {}
/** /**
@@ -37,7 +40,7 @@ class ProcessDownloader implements DownloaderInterface
$this->entityManager->flush(); $this->entityManager->flush();
$downloadPreferences = $downloadEntity->getUser()->getDownloadPreferences(); $downloadPreferences = $downloadEntity->getUser()->getDownloadPreferences();
$path = $this->getDownloadPath($mediaType, $title, $downloadPreferences); $path = $this->getDownloadPath($mediaType, $title, $downloadEntity->getImdbId(), $downloadPreferences);
$processArgs = ['wget', '-O', $downloadEntity->getFilename(), $url]; $processArgs = ['wget', '-O', $downloadEntity->getFilename(), $url];
@@ -103,17 +106,22 @@ class ProcessDownloader implements DownloaderInterface
$this->entityManager->flush(); $this->entityManager->flush();
} }
public function getDownloadPath(string $mediaType, string $title, array $downloadPreferences): string public function getDownloadPath(string $mediaType, string $title, string $imdbId, array $downloadPreferences): string
{ {
$mediaInfo = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand(
$imdbId,
$mediaType,
));
if ($mediaType === 'movies') { if ($mediaType === 'movies') {
if ((bool) $downloadPreferences['movie_folder']->getPreferenceValue() === true) { if ((bool) $downloadPreferences['movie_folder']->getPreferenceValue() === true) {
return $this->mediaFiles->createMovieDirectory($title); return $this->mediaFiles->createMovieDirectory($title, $mediaInfo->media->year);
} }
return $this->mediaFiles->getMoviesPath(); return $this->mediaFiles->getMoviesPath();
} }
if ($mediaType === 'tvshows') { if ($mediaType === 'tvshows') {
return $this->mediaFiles->createTvShowDirectory($title); return $this->mediaFiles->createTvShowDirectory($title, $mediaInfo->media->year);
} }
throw new \Exception("There is no download path for media type: $mediaType"); throw new \Exception("There is no download path for media type: $mediaType");

View File

@@ -11,6 +11,7 @@ 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\Dto\TmdbEpisodeDto;
use App\Tmdb\TmdbClient; use App\Tmdb\TmdbClient;
use App\Tmdb\TmdbResult;
use Carbon\Carbon; use Carbon\Carbon;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@@ -36,18 +37,14 @@ readonly class MonitorTvShowHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface public function handle(CommandInterface $command): ResultInterface
{ {
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler'); $this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
$monitor = $this->monitorRepository->find($command->monitorId); $monitor = $this->monitorRepository->find($command->monitorId);
$this->refreshData($monitor); $showTmdbData = $this->tmdb->tvshowDetails($monitor->getImdbId());
$this->refreshData($monitor, $showTmdbData);
// Check current episodes // Check current episodes
$downloadedEpisodes = $this->mediaFiles $downloadedEpisodes = $this->getDownloadedEpisodes($monitor->getTitle(), $showTmdbData);
->getEpisodes($monitor->getTitle())
->map(fn($episode) => (object)(new PTN())->parse($episode))
->filter(fn($episode) => property_exists($episode, 'episode')
&& property_exists($episode, 'season')
&& null !== $episode->episode
&& 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());
@@ -130,6 +127,33 @@ readonly class MonitorTvShowHandler implements HandlerInterface
); );
} }
private function getDownloadedEpisodes(string $title, TmdbResult $showTmdbData): Map
{
// Episodes in folder w/o the year
$downloadedEpisodes = $this->mediaFiles
->getEpisodes($title)
->map(fn($episode) => (object) new PTN()->parse($episode))
->filter(fn($episode) =>
property_exists($episode, 'episode')
&& property_exists($episode, 'season')
&& null !== $episode->episode
&& null !== $episode->season
);
return $downloadedEpisodes->concat(
// Episodes in folder w/ the year
$this->mediaFiles
->getEpisodes(sprintf("%s (%s)", $title, $showTmdbData->year))
->map(fn($episode) => (object) new PTN()->parse($episode))
->filter(fn($episode) =>
property_exists($episode, 'episode')
&& property_exists($episode, 'season')
&& null !== $episode->episode
&& null !== $episode->season
)
);
}
private function episodeReleasedAfterMonitorCreated( private function episodeReleasedAfterMonitorCreated(
string|DateTimeImmutable $monitorStartDate, string|DateTimeImmutable $monitorStartDate,
TmdbEpisodeDto $episodeInShow TmdbEpisodeDto $episodeInShow
@@ -159,11 +183,11 @@ readonly class MonitorTvShowHandler implements HandlerInterface
]) !== null; ]) !== null;
} }
private function refreshData(Monitor $monitor) private function refreshData(Monitor $monitor, TmdbResult $showTmdbData)
{ {
if (null === $monitor->getPoster()) { if (null === $monitor->getPoster()) {
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"'); $this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster; $poster = $showTmdbData->poster;
if (null !== $poster && "" !== $poster) { if (null !== $poster && "" !== $poster) {
$monitor->setPoster($poster); $monitor->setPoster($poster);
} }

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Monitor;
enum MonitorTypes: string
{
case TV_EPISODE = "tvepisode";
case TV_SEASON = "tvseason";
case TV_SERIES = "tvseries";
}