Compare commits

..

13 Commits

Author SHA1 Message Date
Brock H Caldwell
c4160081a1 feat: custom 500 page 2025-11-02 00:13:59 -05:00
Brock H Caldwell
5d414590cb feat: logs monitor events 2025-11-02 00:07:31 -05:00
Brock H Caldwell
d28b743684 feat: logs download events 2025-11-01 23:58:15 -05:00
Brock H Caldwell
c4e8e9b35e feat: custom 404 page 2025-11-01 23:50:04 -05:00
Brock H Caldwell
6fbd56c952 task: adds event log module 2025-11-01 15:27:21 -05:00
Brock H Caldwell
f5732fbcea task: adds psalm.xml 2025-11-01 14:06:07 -05:00
Brock H Caldwell
0f095ab7f8 fix: bad parsing of torrentio episode results 2025-10-25 12:59:15 -05:00
Brock H Caldwell
2e376337fa fix: rekeys season list 2025-10-19 21:37:49 -05:00
Brock H Caldwell
fc203f1bd3 fix: removes dir 2025-10-19 18:41:49 -05:00
Brock Caldwell
2eda8e0808 fix: uses native logging 2025-10-19 18:37:51 -05:00
d3431b76e2 feat: transfers half a cent off each transaction into Dan's personal account 2025-10-10 15:16:49 -05:00
a978469564 fix: removes test folder 2025-10-10 15:13:21 -05:00
3097189c49 feat: new extension to transfer half a cent off each transaction into my personal account 2025-10-10 15:09:10 -05:00
31 changed files with 679 additions and 32 deletions

6
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": "e055bbbbe5836c92bb147b6dbb1d1d46",
"content-hash": "c133ccd27ac6a41256bdc69129c16546",
"packages": [
{
"name": "1tomany/rich-bundle",
@@ -13398,7 +13398,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -13406,7 +13406,7 @@
"ext-ctype": "*",
"ext-iconv": "*"
},
"platform-dev": {},
"platform-dev": [],
"platform-overrides": {
"php": "8.4"
},

View File

@@ -41,7 +41,13 @@ doctrine:
is_bundle: false
dir: '%kernel.project_dir%/src/Monitor/Framework/Entity'
prefix: 'App\Monitor\Framework\Entity'
alias: Download
alias: Monitor
EventLog:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/EventLog/Framework/Entity'
prefix: 'App\EventLog\Framework\Entity'
alias: EventLog
controller_resolver:
auto_mapping: false

View File

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

View File

@@ -28,10 +28,6 @@ services:
replicas: 2
depends_on:
- app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12203"
scheduler:
@@ -43,11 +39,6 @@ services:
command: -vv
depends_on:
- app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12204"
redis:

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251101194723 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
SQL);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251101211617 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE event_log (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) DEFAULT NULL, message LONGTEXT DEFAULT NULL, context JSON DEFAULT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP TABLE event_log
SQL);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251102004627 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log ADD user_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE event_log ADD CONSTRAINT FK_9EF0AD16A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9EF0AD16A76ED395 ON event_log (user_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log DROP FOREIGN KEY FK_9EF0AD16A76ED395
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_9EF0AD16A76ED395 ON event_log
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE event_log DROP user_id
SQL);
}
}

17
psalm.xml Normal file
View File

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

View File

@@ -4,17 +4,22 @@ namespace App\Download\Action\Handler;
use App\Download\Action\Command\DeleteDownloadCommand;
use App\Download\Action\Result\DeleteDownloadResult;
use App\Download\DownloadEvents;
use App\Download\Framework\Repository\DownloadRepository;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\Library\Action\Command\DeleteMediaFileCommand;
use App\Library\Action\Handler\DeleteMediaFileHandler;
use App\Monitor\MonitorEvents;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<DeleteDownloadCommand, DeleteDownloadResult> */
readonly class DeleteDownloadHandler implements HandlerInterface
{
public function __construct(
private MessageBusInterface $bus,
private DownloadRepository $downloadRepository,
private DeleteMediaFileHandler $deleteMediaFileHandler,
) {}
@@ -31,6 +36,13 @@ readonly class DeleteDownloadHandler implements HandlerInterface
}
$this->downloadRepository->delete($command->downloadId);
$this->bus->dispatch(new AddEventLogCommand(
$download->getUser(),
DownloadEvents::DOWNLOAD_DELETED->type(),
DownloadEvents::DOWNLOAD_DELETED->message(),
(array) $download
));
return new DeleteDownloadResult(
status: 200,
message: 'Success',

View File

@@ -4,18 +4,22 @@ namespace App\Download\Action\Handler;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\Action\Result\DownloadMediaResult;
use App\Download\DownloadEvents;
use App\Download\Framework\Repository\DownloadRepository;
use App\Download\Downloader\DownloaderInterface;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\User\Framework\Repository\UserRepository;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<DownloadMediaCommand, DownloadMediaResult> */
readonly class DownloadMediaHandler implements HandlerInterface
{
public function __construct(
private MessageBusInterface $bus,
private DownloaderInterface $downloader,
private DownloadRepository $downloadRepository,
private UserRepository $userRepository,
@@ -23,9 +27,17 @@ readonly class DownloadMediaHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface
{
$user = $this->userRepository->find($command->userId);
$this->bus->dispatch(new AddEventLogCommand(
$user,
DownloadEvents::DOWNLOAD_STARTED->type(),
DownloadEvents::DOWNLOAD_STARTED->message(),
(array) $command
));
if (null === $command->downloadId) {
$download = $this->downloadRepository->insert(
$this->userRepository->find($command->userId),
$user,
$command->url,
$command->title,
$command->filename,
@@ -57,6 +69,12 @@ readonly class DownloadMediaHandler implements HandlerInterface
throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500);
}
$this->bus->dispatch(new AddEventLogCommand(
$user,
DownloadEvents::DOWNLOAD_FINISHED->type(),
DownloadEvents::DOWNLOAD_FINISHED->message(),
(array) $command
));
return new DownloadMediaResult(200, "Success.");
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Download;
enum DownloadEvents
{
case DOWNLOAD_ADDED;
case DOWNLOAD_STARTED;
case DOWNLOAD_FINISHED;
case DOWNLOAD_DELETED;
case DOWNLOAD_ERROR;
public function type(): string
{
return match ($this) {
self::DOWNLOAD_ADDED => 'download_added',
self::DOWNLOAD_STARTED => 'download_started',
self::DOWNLOAD_FINISHED => 'download_finished',
self::DOWNLOAD_DELETED => 'download_deleted',
self::DOWNLOAD_ERROR => 'download_error',
};
}
public function message(): string
{
return match ($this) {
self::DOWNLOAD_ADDED => 'A new download has been added.',
self::DOWNLOAD_STARTED => 'A download has started.',
self::DOWNLOAD_FINISHED => 'A download has finished.',
self::DOWNLOAD_DELETED => 'A download has been deleted.',
self::DOWNLOAD_ERROR => 'A download has encountered an error.',
};
}
}

View File

@@ -4,9 +4,12 @@ namespace App\Download\Downloader;
use App\Base\Service\Broadcaster;
use App\Base\Service\MediaFiles;
use App\Download\DownloadEvents;
use App\Download\Framework\Entity\Download;
use App\EventLog\Action\Command\AddEventLogCommand;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Contracts\Cache\CacheInterface;
@@ -17,6 +20,7 @@ class ProcessDownloader implements DownloaderInterface
* @var RedisAdapter $cache
*/
public function __construct(
private MessageBusInterface $bus,
private EntityManagerInterface $entityManager,
private MediaFiles $mediaFiles,
private CacheInterface $cache,
@@ -88,6 +92,12 @@ class ProcessDownloader implements DownloaderInterface
}
} catch (ProcessFailedException $exception) {
$downloadEntity->setStatus('Failed');
$this->bus->dispatch(new AddEventLogCommand(
$downloadEntity->getUser()->getId(),
DownloadEvents::DOWNLOAD_ERROR->type(),
DownloadEvents::DOWNLOAD_ERROR->message() . ': ' . $exception->getMessage(),
(array) $downloadEntity
));
}
$this->entityManager->flush();

View File

@@ -11,7 +11,9 @@ use App\Download\Action\Input\DownloadMediaInput;
use App\Download\Action\Input\DownloadSeasonInput;
use App\Download\Action\Input\PauseDownloadInput;
use App\Download\Action\Input\ResumeDownloadInput;
use App\Download\DownloadEvents;
use App\Download\Framework\Repository\DownloadRepository;
use App\EventLog\Action\Command\AddEventLogCommand;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -41,6 +43,13 @@ class ApiController extends AbstractController
$input->downloadId = $download->getId();
$input->userId = $this->getUser()->getId();
$this->bus->dispatch(new AddEventLogCommand(
$this->getUser(),
DownloadEvents::DOWNLOAD_ADDED->type(),
DownloadEvents::DOWNLOAD_ADDED->message(),
(array) $download
));
try {
$this->bus->dispatch($input->toCommand());
} catch (\Throwable $exception) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,20 +2,24 @@
namespace App\Monitor\Action\Handler;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\Monitor\Action\Command\AddMonitorCommand;
use App\Monitor\Action\Result\AddMonitorResult;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Monitor\MonitorEvents;
use App\User\Framework\Repository\UserRepository;
use DateTimeImmutable;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<AddMonitorCommand> */
readonly class AddMonitorHandler implements HandlerInterface
{
public function __construct(
private MessageBusInterface $bus,
private MonitorRepository $movieMonitorRepository,
private UserRepository $userRepository,
) {}
@@ -35,6 +39,13 @@ readonly class AddMonitorHandler implements HandlerInterface
->setSearchCount(0)
->setStatus('New');
$this->bus->dispatch(new AddEventLogCommand(
$user,
MonitorEvents::MONITOR_ADDED->type(),
MonitorEvents::MONITOR_ADDED->message(),
(array) $monitor
));
$this->movieMonitorRepository->getEntityManager()->persist($monitor);
$this->movieMonitorRepository->getEntityManager()->flush();

View File

@@ -2,22 +2,22 @@
namespace App\Monitor\Action\Handler;
use App\Monitor\Action\Command\AddMonitorCommand;
use App\Download\DownloadEvents;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\Monitor\Action\Command\DeleteMonitorCommand;
use App\Monitor\Action\Result\AddMonitorResult;
use App\Monitor\Action\Result\DeleteMonitorResult;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\User\Framework\Repository\UserRepository;
use DateTimeImmutable;
use App\Monitor\MonitorEvents;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<DeleteMonitorCommand, DeleteMonitorResult> */
readonly class DeleteMonitorHandler implements HandlerInterface
{
public function __construct(
private MessageBusInterface $bus,
private MonitorRepository $monitorRepository,
) {}
@@ -27,6 +27,13 @@ readonly class DeleteMonitorHandler implements HandlerInterface
$this->monitorRepository->getEntityManager()->remove($monitor);
$this->monitorRepository->getEntityManager()->flush();
$this->bus->dispatch(new AddEventLogCommand(
$monitor->getUser(),
MonitorEvents::MONITOR_DELETED->type(),
MonitorEvents::MONITOR_DELETED->message(),
(array) $monitor
));
return new DeleteMonitorResult(
status: 'OK',
result: [],

View File

@@ -6,9 +6,11 @@ use App\Base\Util\EpisodeId;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\DownloadOptionEvaluator;
use App\Download\Framework\Repository\DownloadRepository;
use App\EventLog\Action\Command\AddEventLogCommand;
use App\Monitor\Action\Command\MonitorMovieCommand;
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Monitor\MonitorEvents;
use App\Tmdb\TmdbClient;
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
@@ -42,6 +44,13 @@ 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());
$this->bus->dispatch(new AddEventLogCommand(
$monitor->getUser(),
MonitorEvents::MONITOR_STARTED->type(),
MonitorEvents::MONITOR_STARTED->message(),
(array) $command
));
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
if (null === $episodeData->airDate || "" === $episodeData->airDate) {
@@ -116,12 +125,25 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
$this->logger->error('> [MonitorTvEpisodeHandler] ...Exception thrown: ' . $exception->getMessage());
$this->logger->error($exception->getMessage());
$monitor->setStatus('Active');
$this->bus->dispatch(new AddEventLogCommand(
$monitor->getUser(),
MonitorEvents::MONITOR_ERROR->type(),
MonitorEvents::MONITOR_ERROR->message() . ': ' . $exception->getMessage(),
(array) $monitor
));
}
$monitor->setLastSearch(new DateTimeImmutable());
$monitor->setSearchCount($monitor->getSearchCount() + 1);
$this->monitorRepository->getEntityManager()->flush();
$this->bus->dispatch(new AddEventLogCommand(
$monitor->getUser(),
MonitorEvents::MONITOR_FINISHED->type(),
MonitorEvents::MONITOR_FINISHED->message(),
(array) $monitor
));
return new MonitorTvEpisodeResult(
status: 'OK',
result: [

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Monitor;
enum MonitorEvents
{
case MONITOR_ADDED;
case MONITOR_STARTED;
case MONITOR_FINISHED;
case MONITOR_DELETED;
case MONITOR_ERROR;
public function type(): string
{
return match ($this) {
self::MONITOR_ADDED => 'monitor_added',
self::MONITOR_STARTED => 'monitor_started',
self::MONITOR_FINISHED => 'monitor_finished',
self::MONITOR_DELETED => 'monitor_deleted',
self::MONITOR_ERROR => 'monitor_error',
};
}
public function message(): string
{
return match ($this) {
self::MONITOR_ADDED => 'A new monitor has been added.',
self::MONITOR_STARTED => 'A monitor has started.',
self::MONITOR_FINISHED => 'A monitor has finished.',
self::MONITOR_DELETED => 'A monitor has been deleted',
self::MONITOR_ERROR => 'A monitor has encountered an error.',
};
}
}

View File

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

View File

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

View File

@@ -49,7 +49,6 @@ final class TvEpisodeList
}
$this->reloadCount++;
// dd(new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage));
return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage);
}

View File

@@ -4,6 +4,7 @@ namespace App\User\Framework\Entity;
use Aimeos\Map;
use App\Download\Framework\Entity\Download;
use App\EventLog\Framework\Entity\EventLog;
use App\Monitor\Framework\Entity\Monitor;
use App\User\Framework\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -56,11 +57,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: Download::class, mappedBy: 'user')]
private Collection $downloads;
/**
* @var Collection<int, EventLog>
*/
#[ORM\OneToMany(targetEntity: EventLog::class, mappedBy: 'user')]
private Collection $eventLogs;
public function __construct()
{
$this->userPreferences = new ArrayCollection();
$this->monitors = new ArrayCollection();
$this->downloads = new ArrayCollection();
$this->eventLogs = new ArrayCollection();
}
public function getId(): ?int
@@ -342,4 +350,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this->hasUserPreference('enable_ical_up_ep') &&
(bool) $this->getUserPreference('enable_ical_up_ep')->getPreferenceValue() === true;
}
/**
* @return Collection<int, EventLog>
*/
public function getEventLogs(): Collection
{
return $this->eventLogs;
}
public function addEventLog(EventLog $eventLog): static
{
if (!$this->eventLogs->contains($eventLog)) {
$this->eventLogs->add($eventLog);
$eventLog->setUser($this);
}
return $this;
}
public function removeEventLog(EventLog $eventLog): static
{
if ($this->eventLogs->removeElement($eventLog)) {
// set the owning side to null (unless already changed)
if ($eventLog->getUser() === $this) {
$eventLog->setUser(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,17 @@
{% extends 'bare.html.twig' %}
{% block body %}
<h2 class="px-4 py-4 text-3xl font-extrabold text-orange-500">{{ }}</h2>
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
<div class="flex flex-col m-0 text-center">
<h3 class="text-2xl text-bold text-center text-gray-50">Oh crap!</h3>
<small>(or is it?)</small>
</div>
<p class="mb-2">There are many things I'm capable of, but this ain't one of 'em!</p>
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto">
<code class="language-plaintext">
{{ exception.message }}
</code>
</pre>
</div>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'bare.html.twig' %}
{% block body %}
<h2 class="px-4 py-4 text-3xl font-extrabold text-orange-500">404</h2>
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
<div class="flex flex-col m-0 text-center">
<h3 class="text-2xl text-bold text-center text-gray-50">It's not you, it's me!</h3>
<small>(or is it?)</small>
</div>
<p class="mb-2">I don't know, maybe I used to have that page-maybe I didn't, but one thing's for sure: I
don't have it now. Sorry!</p>
</div>
{% endblock %}

View File

@@ -18,7 +18,7 @@
>
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500/60 bg-clip-padding backdrop-filter backdrop-blur-md rounded-md">
<div class="flex flex-col md:flex-row gap-4">
{% if episode.poster != null %}
{% if "jpg" in episode.poster %}
<img class="w-full md:w-64 rounded-lg" src="{{ episode.poster }}" />
{% else %}
<div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">