Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f291aa147 | ||
|
|
c4160081a1 | ||
|
|
5d414590cb | ||
|
|
d28b743684 | ||
|
|
c4e8e9b35e | ||
|
|
6fbd56c952 | ||
|
|
f5732fbcea | ||
|
|
0f095ab7f8 | ||
|
|
2e376337fa | ||
|
|
fc203f1bd3 | ||
|
|
2eda8e0808 | ||
| d3431b76e2 | |||
| a978469564 | |||
| 3097189c49 | |||
| 6f11de70e0 | |||
| 4e06fe6636 | |||
| fd46abf58f | |||
| 2237a45d6f | |||
| 846de2c257 | |||
| d01b725435 | |||
| 7562597629 | |||
| deb0333635 | |||
| c8e190f9e8 | |||
| 538fde40fe |
@@ -13,34 +13,7 @@ export default class extends Controller {
|
||||
tmdbId: String,
|
||||
imdbId: String,
|
||||
title: String,
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Called once when the controller is first instantiated (per element)
|
||||
|
||||
// Here you can initialize variables, create scoped callables for event
|
||||
// listeners, instantiate external libraries, etc.
|
||||
// this._fooBar = this.fooBar.bind(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Called every time the controller is connected to the DOM
|
||||
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
|
||||
|
||||
// Here you can add event listeners on the element or target elements,
|
||||
// add or remove classes, attributes, dispatch custom events, etc.
|
||||
// this.fooTarget.addEventListener('click', this._fooBar)
|
||||
}
|
||||
|
||||
// Add custom controller actions here
|
||||
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||
|
||||
disconnect() {
|
||||
// Called anytime its element is disconnected from the DOM
|
||||
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||
|
||||
// Here you should remove all event listeners added in "connect()"
|
||||
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||
season: Number,
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -53,34 +26,13 @@ export default class extends Controller {
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvshows',
|
||||
season: this.seasonValue
|
||||
});
|
||||
if (this.hasDialogOutlet) {
|
||||
this.dialogOutlet.close();
|
||||
}
|
||||
}
|
||||
|
||||
async monitorSeason() {
|
||||
await this.makeMonitor({
|
||||
tmdbId: this.tmdbIdValue,
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvseason',
|
||||
season: this.resultFilterOutlet.activeFilter['season'],
|
||||
});
|
||||
}
|
||||
|
||||
async monitorEpisode() {
|
||||
// ToDo: figure out how to set episode
|
||||
await this.makeMonitor({
|
||||
tmdbId: this.tmdbIdValue,
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvepisode',
|
||||
season: this.resultFilterOutlet.activeFilter['season'],
|
||||
episode: '',
|
||||
});
|
||||
}
|
||||
|
||||
async makeMonitor(body) {
|
||||
const response = await fetch('/api/monitor', {
|
||||
method: 'POST',
|
||||
@@ -90,7 +42,6 @@ export default class extends Controller {
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
6
composer.lock
generated
6
composer.lock
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -15,10 +15,6 @@ services:
|
||||
- mercure_config:/config
|
||||
depends_on:
|
||||
- database
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12202"
|
||||
|
||||
|
||||
worker:
|
||||
@@ -32,10 +28,6 @@ services:
|
||||
replicas: 2
|
||||
depends_on:
|
||||
- app
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12203"
|
||||
|
||||
|
||||
scheduler:
|
||||
@@ -47,11 +39,6 @@ services:
|
||||
command: -vv
|
||||
depends_on:
|
||||
- app
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12204"
|
||||
|
||||
|
||||
|
||||
redis:
|
||||
|
||||
@@ -21,7 +21,7 @@ final class Version20250831013403 extends AbstractMigration
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE sessions
|
||||
DROP TABLE IF EXISTS sessions
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
||||
|
||||
35
migrations/Version20251101194723.php
Normal file
35
migrations/Version20251101194723.php
Normal 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);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20251101211617.php
Normal file
35
migrations/Version20251101211617.php
Normal 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);
|
||||
}
|
||||
}
|
||||
47
migrations/Version20251102004627.php
Normal file
47
migrations/Version20251102004627.php
Normal 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
17
psalm.xml
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Download/DownloadEvents.php
Normal file
34
src/Download/DownloadEvents.php
Normal 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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
17
src/EventLog/Action/Command/AddEventLogCommand.php
Normal file
17
src/EventLog/Action/Command/AddEventLogCommand.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
29
src/EventLog/Action/Handler/AddEventLogHandler.php
Normal file
29
src/EventLog/Action/Handler/AddEventLogHandler.php
Normal 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);
|
||||
}
|
||||
}
|
||||
36
src/EventLog/Action/Input/AddEventLogInput.php
Normal file
36
src/EventLog/Action/Input/AddEventLogInput.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
14
src/EventLog/Action/Result/AddEventLogResult.php
Normal file
14
src/EventLog/Action/Result/AddEventLogResult.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
28
src/EventLog/Framework/Controller/EventLogController.php
Normal file
28
src/EventLog/Framework/Controller/EventLogController.php
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
src/EventLog/Framework/Entity/EventLog.php
Normal file
79
src/EventLog/Framework/Entity/EventLog.php
Normal 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;
|
||||
}
|
||||
}
|
||||
35
src/EventLog/Framework/Repository/EventLogRepository.php
Normal file
35
src/EventLog/Framework/Repository/EventLogRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,13 +44,31 @@ 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->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$monitor->getUser(),
|
||||
MonitorEvents::MONITOR_STARTED->type(),
|
||||
MonitorEvents::MONITOR_STARTED->message(),
|
||||
(array) $command
|
||||
));
|
||||
|
||||
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
||||
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
||||
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||
|
||||
if (null === $episodeData->airDate || "" === $episodeData->airDate) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode does not have an air date, skipping for now');
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'message' => 'No change',
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
||||
if (null === $monitor->getAirDate()) {
|
||||
$monitor->setAirDate(Carbon::parse($episodeData->airDate));
|
||||
}
|
||||
|
||||
if (Carbon::createFromTimestamp($episodeData->airDate) > Carbon::today('UTC')) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
@@ -105,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: [
|
||||
|
||||
@@ -30,7 +30,8 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
private MediaFiles $mediaFiles,
|
||||
private LoggerInterface $logger,
|
||||
private TmdbClient $tmdb,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
@@ -40,79 +41,77 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
// Check current episodes
|
||||
$downloadedEpisodes = $this->mediaFiles
|
||||
->getEpisodes($monitor->getTitle())
|
||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
||||
->filter(fn ($episode) =>
|
||||
property_exists($episode, 'episode')
|
||||
->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());
|
||||
|
||||
// Compare against list from TMDB
|
||||
$episodesInShow = Map::from(
|
||||
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
|
||||
)->flat(1);
|
||||
)->flat(1)
|
||||
->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason())
|
||||
->values();
|
||||
|
||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
||||
|
||||
$episodeMonitors = [];
|
||||
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->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) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the episode is already downloaded
|
||||
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||
if (true === $episodeExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for existing monitors
|
||||
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||
if (true === $monitorExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the monitor
|
||||
$episodeMonitor = (new Monitor())
|
||||
->setParent($monitor)
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($episode->seasonNumber)
|
||||
->setEpisode($episode->episodeNumber)
|
||||
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$episodeMonitors[] = $episodeMonitor;
|
||||
|
||||
// Immediately run the monitor
|
||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||
$this->monitorTvEpisodeHandler->handle($command);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
||||
// 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->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) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the episode is already downloaded
|
||||
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||
if (true === $episodeExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for existing monitors
|
||||
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||
if (true === $monitorExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the monitor
|
||||
$episodeMonitor = (new Monitor())
|
||||
->setParent($monitor)
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($episode->seasonNumber)
|
||||
->setEpisode($episode->episodeNumber)
|
||||
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$episodeMonitors[] = $episodeMonitor;
|
||||
|
||||
// Immediately run the monitor
|
||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||
$this->monitorTvEpisodeHandler->handle($command);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
||||
}
|
||||
|
||||
// Set the status to Active, so it will be re-executed.
|
||||
@@ -130,8 +129,10 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
);
|
||||
}
|
||||
|
||||
private function episodeReleasedAfterMonitorCreated(string|DateTimeImmutable $monitorStartDate, TmdbEpisodeDto $episodeInShow): bool
|
||||
{
|
||||
private function episodeReleasedAfterMonitorCreated(
|
||||
string|DateTimeImmutable $monitorStartDate,
|
||||
TmdbEpisodeDto $episodeInShow
|
||||
): bool {
|
||||
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
||||
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
||||
return $episodeAirDate >= $monitorStartDate;
|
||||
@@ -140,8 +141,8 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
||||
{
|
||||
return $downloadedEpisodes->filter(
|
||||
fn (object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||
&& $episode->season === $episodeInShow->seasonNumber
|
||||
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||
&& $episode->season === $episodeInShow->seasonNumber
|
||||
)->count() > 0;
|
||||
}
|
||||
|
||||
|
||||
34
src/Monitor/MonitorEvents.php
Normal file
34
src/Monitor/MonitorEvents.php
Normal 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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ class GetMediaInfoHandler implements HandlerInterface
|
||||
$handlers = [
|
||||
MediaType::Movie->value => 'getMovieDetails',
|
||||
MediaType::TvShow->value => 'getTvshowDetails',
|
||||
MediaType::TvEpisode->value => 'getTvshowDetails',
|
||||
];
|
||||
$handler = $handlers[$command->mediaType];
|
||||
$media = $this->$handler($command);
|
||||
|
||||
@@ -38,6 +38,10 @@ class TmdbTvShowResultDenormalizer extends TmdbResultDenormalizer implements Den
|
||||
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
||||
$result->mediaType = MediaType::TvShow->value;
|
||||
|
||||
if (is_array($result->episodes)) {
|
||||
$result->latestSeason = array_key_last($result->episodes);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -170,13 +172,13 @@ class TmdbClient
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
||||
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
||||
{
|
||||
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
|
||||
return $this->parseResult(
|
||||
$result,
|
||||
MediaType::TvEpisode->value,
|
||||
$result['external_ids']['imdb_id']
|
||||
$showImdbId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,5 +56,6 @@ class TmdbResult
|
||||
public ?array $producers = null,
|
||||
public ?int $runtime = null,
|
||||
public ?int $numberSeasons = null,
|
||||
public ?int $latestSeason = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->season, $command->episode);
|
||||
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->imdbId, $command->season, $command->episode);
|
||||
$parentShow = $this->tmdb->tvshowDetails($command->imdbId);
|
||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
17
templates/bundles/TwigBundle/Exception/error.html.twig
Normal file
17
templates/bundles/TwigBundle/Exception/error.html.twig
Normal 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 %}
|
||||
13
templates/bundles/TwigBundle/Exception/error404.html.twig
Normal file
13
templates/bundles/TwigBundle/Exception/error404.html.twig
Normal 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 %}
|
||||
@@ -14,18 +14,10 @@
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
||||
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
||||
class="mr-1 hover:underline rounded-md"
|
||||
>
|
||||
|
||||
{% if monitor.monitorType == "movies" %}
|
||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType} %}
|
||||
{% set route = path('app_search_result', routeParams) %}
|
||||
{% else %}
|
||||
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
|
||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType, season: episodeIdDto.season, episode: episodeIdDto.episode} %}
|
||||
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
|
||||
{% endif %}
|
||||
<a href="{{ route }}"
|
||||
class="mr-1 hover:underline rounded-md">
|
||||
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
|
||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type, season: episodeIdDto.season} %}
|
||||
<a href="{{ path('app_search_result', routeParams) }}"
|
||||
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate dark:text-white">
|
||||
{{ monitor.title }}
|
||||
</a>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
class="episode-list flex flex-col gap-4"
|
||||
>
|
||||
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
|
||||
{% if this.getEpisodes().items != null %}
|
||||
{% for episode in this.getEpisodes().items %}
|
||||
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
|
||||
show-title="{{ this.title }}"
|
||||
@@ -17,7 +18,7 @@
|
||||
>
|
||||
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500/60 bg-clip-padding backdrop-filter backdrop-blur-md rounded-md">
|
||||
<div class="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">
|
||||
@@ -85,6 +86,7 @@
|
||||
</div>
|
||||
</episode-container>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% set paginator = this.episodes %}
|
||||
{% include 'partial/tv-episode-list-paginator.html.twig' %}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
tmdbId: results.media.tmdbId,
|
||||
imdbId: results.media.imdbId,
|
||||
title: results.media.title,
|
||||
season: results.media.latestSeason,
|
||||
})}}
|
||||
data-monitor-button-result-filter-outlet="#filter"
|
||||
data-monitor-button-dialog-outlet=".monitor-modal"
|
||||
|
||||
Reference in New Issue
Block a user