Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fb513bfbd | |||
| 2384bb2414 | |||
| 7c8fa0c439 | |||
| 97aa8d8982 | |||
| a88720fe7e | |||
| 8a12303470 | |||
| 13b9047841 | |||
| 8c0ec98c20 | |||
| 2c9138290a | |||
| c1a6cddb8f | |||
| 64d3fbbddb | |||
| 32389cb27a | |||
| 5e48fdb978 | |||
| 5f54e48b3f |
46
assets/controllers/action_button_controller.js
Normal file
46
assets/controllers/action_button_controller.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||||
|
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
default() {
|
||||||
|
console.log('Looks like you need to add an action to your action button...')
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorDispatch() {
|
||||||
|
fetch('/api/monitor/dispatch')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Controller } from '@hotwired/stimulus';
|
|||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['button', 'options']
|
static targets = ['button', 'options']
|
||||||
static outlets = ['result-filter']
|
static outlets = ['result-filter', 'dialog']
|
||||||
static values = {
|
static values = {
|
||||||
tmdbId: String,
|
tmdbId: String,
|
||||||
imdbId: String,
|
imdbId: String,
|
||||||
@@ -54,6 +54,9 @@ export default class extends Controller {
|
|||||||
title: this.titleValue,
|
title: this.titleValue,
|
||||||
monitorType: 'tvshows',
|
monitorType: 'tvshows',
|
||||||
});
|
});
|
||||||
|
if (this.hasDialogOutlet) {
|
||||||
|
this.dialogOutlet.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async monitorSeason() {
|
async monitorSeason() {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default class extends Controller {
|
|||||||
static outlets = ['movie-results', 'tv-results', 'tv-episode-list']
|
static outlets = ['movie-results', 'tv-results', 'tv-episode-list']
|
||||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
||||||
static values = {
|
static values = {
|
||||||
|
'imdbId': String,
|
||||||
'media-type': String,
|
'media-type': String,
|
||||||
'episodes': Array,
|
'episodes': Array,
|
||||||
'reverseMappedQualities': Object,
|
'reverseMappedQualities': Object,
|
||||||
@@ -156,6 +157,14 @@ export default class extends Controller {
|
|||||||
this.selectAllTarget.checked = false;
|
this.selectAllTarget.checked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadSeason() {
|
||||||
|
fetch(`/api/download/season/${this.imdbIdValue}/${this.activeFilter['season']}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectAllEpisodes() {
|
selectAllEpisodes() {
|
||||||
this.tvResultsOutlets.forEach((episode) => {
|
this.tvResultsOutlets.forEach((episode) => {
|
||||||
if (episode.isActive()) {
|
if (episode.isActive()) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume media_cache -vv --time-limit=3600
|
command: php /app/bin/console messenger:consume async -vv --time-limit=3600
|
||||||
|
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ framework:
|
|||||||
# Route your messages to the transports
|
# Route your messages to the transports
|
||||||
# 'App\Message\YourMessage': async
|
# 'App\Message\YourMessage': async
|
||||||
'App\Download\Action\Command\DownloadMediaCommand': async
|
'App\Download\Action\Command\DownloadMediaCommand': async
|
||||||
|
'App\Download\Action\Command\DownloadSeasonCommand': async
|
||||||
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': async
|
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': async
|
||||||
'App\Monitor\Action\Command\MonitorTvSeasonCommand': async
|
'App\Monitor\Action\Command\MonitorTvSeasonCommand': async
|
||||||
'App\Monitor\Action\Command\MonitorTvShowCommand': async
|
'App\Monitor\Action\Command\MonitorTvShowCommand': async
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# or pass your certificates into the 'app' container.
|
# or pass your certificates into the 'app' container.
|
||||||
# Please omit any trailing slashes. The APP_URL is
|
# Please omit any trailing slashes. The APP_URL is
|
||||||
# used to generate the Mercure URL behind the scenes.
|
# used to generate the Mercure URL behind the scenes.
|
||||||
APP_URL="https://torsearch.idocode.io"
|
APP_URL="https://dev.caldwell.digital"
|
||||||
APP_SECRET="70169beadfbc8101c393cbfbba27a313"
|
APP_SECRET="70169beadfbc8101c393cbfbba27a313"
|
||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.9.1
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- $PWD/../../bash/caddy:/etc/caddy
|
||||||
|
- $PWD/../../bash/certs:/etc/ssl
|
||||||
# The "entrypoint" into the application. This reverse proxy
|
# The "entrypoint" into the application. This reverse proxy
|
||||||
# proxies traffic back to their respective services. If not
|
# proxies traffic back to their respective services. If not
|
||||||
# running behind a reverse proxy inject your SSL certificates
|
# running behind a reverse proxy inject your SSL certificates
|
||||||
@@ -12,8 +24,8 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/media/downloads/movies:/var/download/movies
|
- ./downloads/movies:/var/download/movies
|
||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- ./downloads/tvshows:/var/download/tvshows
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
@@ -32,8 +44,8 @@ services:
|
|||||||
worker:
|
worker:
|
||||||
image: code.caldwell.digital/home/torsearch-worker:latest
|
image: code.caldwell.digital/home/torsearch-worker:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/media/downloads/movies:/var/download/movies
|
- ./downloads/movies:/var/download/movies
|
||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- ./downloads/tvshows:/var/download/tvshows
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
command: -vvv
|
command: -vvv
|
||||||
@@ -52,8 +64,8 @@ services:
|
|||||||
scheduler:
|
scheduler:
|
||||||
image: code.caldwell.digital/home/torsearch-scheduler:latest
|
image: code.caldwell.digital/home/torsearch-scheduler:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/media/downloads/movies:/var/download/movies
|
- ./downloads/movies:/var/download/movies
|
||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- ./downloads/tvshows:/var/download/tvshows
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
35
migrations/Version20250708033046.php
Normal file
35
migrations/Version20250708033046.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 Version20250708033046 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 monitor ADD only_future TINYINT(1) NOT NULL DEFAULT 1
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP only_future
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Base\Framework\Command;
|
namespace App\Base\Framework\Command;
|
||||||
|
|
||||||
|
use App\User\Framework\Entity\Preference;
|
||||||
use App\User\Framework\Entity\UserPreference;
|
use App\User\Framework\Entity\UserPreference;
|
||||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||||
use App\User\Framework\Repository\PreferencesRepository;
|
use App\User\Framework\Repository\PreferencesRepository;
|
||||||
@@ -50,17 +51,23 @@ class SeedDatabaseCommand extends Command
|
|||||||
$preferences = $this->getPreferences();
|
$preferences = $this->getPreferences();
|
||||||
|
|
||||||
foreach ($preferences as $preference) {
|
foreach ($preferences as $preference) {
|
||||||
if ($this->preferenceRepository->find($preference['id'])) {
|
$isNewRecord = false;
|
||||||
continue;
|
$preferenceRecord = $this->preferenceRepository->findOneBy(['id' => $preference['id']]);
|
||||||
|
if (null === $preferenceRecord) {
|
||||||
|
$isNewRecord = true;
|
||||||
|
$preferenceRecord = new Preference();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->preferenceRepository->getEntityManager()->persist((new \App\User\Framework\Entity\Preference())
|
$preferenceRecord
|
||||||
->setId($preference['id'])
|
->setId($preference['id'])
|
||||||
->setName($preference['name'])
|
->setName($preference['name'])
|
||||||
->setDescription($preference['description'])
|
->setDescription($preference['description'])
|
||||||
->setEnabled($preference['enabled'])
|
->setEnabled($preference['enabled'])
|
||||||
->setType($preference['type'])
|
->setType($preference['type']);
|
||||||
);
|
|
||||||
|
if (true === $isNewRecord) {
|
||||||
|
$this->preferenceRepository->getEntityManager()->persist($preferenceRecord);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->preferenceRepository->getEntityManager()->flush();
|
$this->preferenceRepository->getEntityManager()->flush();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Monitor\Service;
|
namespace App\Base\Service;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
18
src/Download/Action/Command/DownloadSeasonCommand.php
Normal file
18
src/Download/Action/Command/DownloadSeasonCommand.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements CommandInterface<DownloadSeasonCommand>
|
||||||
|
*/
|
||||||
|
class DownloadSeasonCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $userId,
|
||||||
|
public int $season,
|
||||||
|
public string $imdbId,
|
||||||
|
public string $mediaType = 'tvshows',
|
||||||
|
) {}
|
||||||
|
}
|
||||||
106
src/Download/Action/Handler/DownloadSeasonHandler.php
Normal file
106
src/Download/Action/Handler/DownloadSeasonHandler.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Handler;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
|
use App\Download\Action\Command\DownloadSeasonCommand;
|
||||||
|
use App\Download\Action\Result\DownloadMediaResult;
|
||||||
|
use App\Download\Action\Result\DownloadSeasonResult;
|
||||||
|
use App\Download\DownloadOptionEvaluator;
|
||||||
|
use App\Tmdb\Tmdb;
|
||||||
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
|
use App\User\Framework\Repository\UserRepository;
|
||||||
|
use Nihilarr\PTN;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
|
/** @implements HandlerInterface<DownloadSeasonCommand, DownloadMediaResult> */
|
||||||
|
readonly class DownloadSeasonHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MediaFiles $mediaFiles,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
private Tmdb $tmdb,
|
||||||
|
private MessageBusInterface $bus,
|
||||||
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
|
{
|
||||||
|
$series = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] Executing DownloadTvSeasonHandler for "' . $series->title . '" season ' . $command->season);
|
||||||
|
|
||||||
|
$episodesInSeason = Map::from($series->episodes[$command->season]);
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ...Found ' . count($episodesInSeason) . ' episodes in season ' . $command->season);
|
||||||
|
|
||||||
|
$downloadCommands = [];
|
||||||
|
foreach ($episodesInSeason as $episode) {
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode['episode_number']);
|
||||||
|
|
||||||
|
$results = $this->getTvShowOptionsHandler->handle(
|
||||||
|
new GetTvShowOptionsCommand(
|
||||||
|
$series->tmdbId,
|
||||||
|
$command->imdbId,
|
||||||
|
$command->season,
|
||||||
|
$episode['episode_number']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found ' . count($results->results) . ' total download options, beginning evaluation');
|
||||||
|
|
||||||
|
$userPreferences = UserPreferencesFactory::createFromUser(
|
||||||
|
$this->userRepository->findOneBy(['id' => $command->userId])
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->downloadOptionEvaluator->evaluateOptions($results->results, $userPreferences);
|
||||||
|
|
||||||
|
if (null !== $result) {
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode['episode_number']);
|
||||||
|
$downloadCommand = new DownloadMediaCommand(
|
||||||
|
$result->url,
|
||||||
|
$series->title,
|
||||||
|
$result->filename,
|
||||||
|
'tvshows',
|
||||||
|
$command->imdbId,
|
||||||
|
$command->userId,
|
||||||
|
);
|
||||||
|
$this->bus->dispatch($downloadCommand);
|
||||||
|
$downloadCommands[] = $downloadCommand;
|
||||||
|
} else {
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 0 matching results');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DownloadSeasonResult(
|
||||||
|
status: 200,
|
||||||
|
message: 'Success',
|
||||||
|
data: ['downloads' => $downloadCommands],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDownloadedEpisodes(string $title)
|
||||||
|
{
|
||||||
|
// Check current episodes
|
||||||
|
$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
|
||||||
|
)
|
||||||
|
->rekey(fn($episode) => $episode->episode);
|
||||||
|
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Download/Action/Input/DownloadSeasonInput.php
Normal file
37
src/Download/Action/Input/DownloadSeasonInput.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Input;
|
||||||
|
|
||||||
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
|
use App\Download\Action\Command\DownloadSeasonCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/** @implements InputInterface<DownloadSeasonInput> */
|
||||||
|
class DownloadSeasonInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceRoute('imdbId')]
|
||||||
|
public string $imdbId,
|
||||||
|
|
||||||
|
#[SourceRoute('season')]
|
||||||
|
public int $season,
|
||||||
|
|
||||||
|
#[SourceRequest('mediaType')]
|
||||||
|
public string $mediaType = 'tvshows',
|
||||||
|
|
||||||
|
public ?int $userId = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): CommandInterface
|
||||||
|
{
|
||||||
|
return new DownloadSeasonCommand(
|
||||||
|
$this->userId,
|
||||||
|
$this->season,
|
||||||
|
$this->imdbId,
|
||||||
|
$this->mediaType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Download/Action/Result/DownloadSeasonResult.php
Normal file
15
src/Download/Action/Result/DownloadSeasonResult.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
/** @implements ResultInterface<DownloadSeasonResult> */
|
||||||
|
class DownloadSeasonResult implements ResultInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $status,
|
||||||
|
public string $message,
|
||||||
|
public array $data,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Monitor\Service;
|
namespace App\Download;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Torrentio\Result\TorrentioResult;
|
use App\Torrentio\Result\TorrentioResult;
|
||||||
|
use App\User\Dto\UserPreferences;
|
||||||
|
|
||||||
class MonitorOptionEvaluator
|
class DownloadOptionEvaluator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Monitor $monitor
|
* @param Monitor $monitor
|
||||||
@@ -14,7 +15,7 @@ class MonitorOptionEvaluator
|
|||||||
* @return TorrentioResult|null
|
* @return TorrentioResult|null
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function evaluateOptions(Monitor $monitor, array $results): ?TorrentioResult
|
public function evaluateOptions(array $results, UserPreferences $userPreferences): ?TorrentioResult
|
||||||
{
|
{
|
||||||
$sizeLow = 000;
|
$sizeLow = 000;
|
||||||
$sizeHigh = 4096;
|
$sizeHigh = 4096;
|
||||||
@@ -22,35 +23,33 @@ class MonitorOptionEvaluator
|
|||||||
$bestMatches = [];
|
$bestMatches = [];
|
||||||
$matches = [];
|
$matches = [];
|
||||||
|
|
||||||
$userPreferences = $monitor->getUser()->getUserPreferenceValues();
|
|
||||||
|
|
||||||
foreach ($results as $result) {
|
foreach ($results as $result) {
|
||||||
if (!in_array($userPreferences['language'], $result->languages)) {
|
if (!in_array($userPreferences->language, $result->languages)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result->resolution === $userPreferences['resolution']
|
if ($result->resolution === $userPreferences->resolution
|
||||||
&& $result->codec === $userPreferences['codec']
|
&& $result->codec === $userPreferences->codec
|
||||||
) {
|
) {
|
||||||
$bestMatches[] = $result;
|
$bestMatches[] = $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userPreferences['resolution'] === '2160p'
|
if ($userPreferences->resolution === '2160p'
|
||||||
&& $userPreferences['codec'] === $result->codec
|
&& $userPreferences->codec === $result->codec
|
||||||
&& $result->resolution === '1080p'
|
&& $result->resolution === '1080p'
|
||||||
) {
|
) {
|
||||||
$matches[] = $result;
|
$matches[] = $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userPreferences['codec'] === 'h264'
|
if ($userPreferences->codec === 'h264'
|
||||||
&& $userPreferences['resolution'] === $result->resolution
|
&& $userPreferences->resolution === $result->resolution
|
||||||
&& $result->codec === 'h265'
|
&& $result->codec === 'h265'
|
||||||
) {
|
) {
|
||||||
$matches[] = $result;
|
$matches[] = $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($userPreferences['codec'] === null )
|
if (($userPreferences->codec === null )
|
||||||
&& ($userPreferences['resolution'] === null )) {
|
&& ($userPreferences->resolution === null )) {
|
||||||
$matches[] = $result;
|
$matches[] = $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Download\Downloader;
|
namespace App\Download\Downloader;
|
||||||
|
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\Monitor\Service\MediaFiles;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ namespace App\Download\Framework\Controller;
|
|||||||
|
|
||||||
use App\Base\Util\Broadcaster;
|
use App\Base\Util\Broadcaster;
|
||||||
use App\Download\Action\Handler\DeleteDownloadHandler;
|
use App\Download\Action\Handler\DeleteDownloadHandler;
|
||||||
|
use App\Download\Action\Handler\DownloadSeasonHandler;
|
||||||
use App\Download\Action\Handler\PauseDownloadHandler;
|
use App\Download\Action\Handler\PauseDownloadHandler;
|
||||||
use App\Download\Action\Handler\ResumeDownloadHandler;
|
use App\Download\Action\Handler\ResumeDownloadHandler;
|
||||||
use App\Download\Action\Input\DeleteDownloadInput;
|
use App\Download\Action\Input\DeleteDownloadInput;
|
||||||
use App\Download\Action\Input\DownloadMediaInput;
|
use App\Download\Action\Input\DownloadMediaInput;
|
||||||
|
use App\Download\Action\Input\DownloadSeasonInput;
|
||||||
use App\Download\Action\Input\PauseDownloadInput;
|
use App\Download\Action\Input\PauseDownloadInput;
|
||||||
use App\Download\Action\Input\ResumeDownloadInput;
|
use App\Download\Action\Input\ResumeDownloadInput;
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use Nihilarr\PTN;
|
use Nihilarr\PTN;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -105,4 +108,18 @@ class ApiController extends AbstractController
|
|||||||
|
|
||||||
return $this->json(['status' => 200, 'message' => 'Download Resumed']);
|
return $this->json(['status' => 200, 'message' => 'Download Resumed']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/download/season/{imdbId}/{season}', name: 'api_download_season', methods: ['GET'])]
|
||||||
|
public function downloadSeason(
|
||||||
|
DownloadSeasonInput $input,
|
||||||
|
): Response {
|
||||||
|
$input->userId = $this->getUser()->getId();
|
||||||
|
$this->bus->dispatch($input->toCommand());
|
||||||
|
$this->broadcaster->alert(
|
||||||
|
title: 'Success',
|
||||||
|
message: "Your download for season $input->season has been added to the queue.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json(['status' => 200, 'message' => 'Download Resumed']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ readonly class MonitorMovieHandler implements HandlerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private MonitorRepository $movieMonitorRepository,
|
private MonitorRepository $movieMonitorRepository,
|
||||||
private GetMovieOptionsHandler $getMovieOptionsHandler,
|
private GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||||
private MonitorOptionEvaluator $monitorOptionEvaluator,
|
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace App\Monitor\Action\Handler;
|
namespace App\Monitor\Action\Handler;
|
||||||
|
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
|
use App\Download\DownloadOptionEvaluator;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MonitorOptionEvaluator;
|
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
@@ -25,7 +25,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
private MonitorOptionEvaluator $monitorOptionEvaluator,
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
@@ -65,10 +65,10 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
|
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found ' . count($results->results) . ' total download options, beginning evaluation');
|
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found ' . count($results->results) . ' total download options, beginning evaluation');
|
||||||
|
|
||||||
$result = $this->monitorOptionEvaluator->evaluateOptions($monitor, $results->results);
|
$result = $this->downloadOptionEvaluator->evaluateOptions($results->results, UserPreferencesFactory::createFromUser($monitor->getUser()));
|
||||||
|
|
||||||
if (null !== $result) {
|
if (null !== $result) {
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found 1 matching result found: dispatching DownloadMediaCommand for "' . $result->title . '"', ['filter' => UserPreferencesFactory::createFromUser($monitor->getUser())]);
|
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found 1 matching result found: dispatching DownloadMediaCommand for "' . $result->title . '"');
|
||||||
$this->bus->dispatch(new DownloadMediaCommand(
|
$this->bus->dispatch(new DownloadMediaCommand(
|
||||||
$result->url,
|
$result->url,
|
||||||
$monitor->getTitle(),
|
$monitor->getTitle(),
|
||||||
@@ -83,17 +83,16 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found 0 matching results found, monitor will run at next interval');
|
$this->logger->info('> [MonitorTvEpisodeHandler] ...Found 0 matching results found, monitor will run at next interval');
|
||||||
$monitor->setStatus('Active');
|
$monitor->setStatus('Active');
|
||||||
}
|
}
|
||||||
|
|
||||||
$monitor->setLastSearch(new DateTimeImmutable());
|
|
||||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$this->logger->error('> [MonitorTvEpisodeHandler] ...Exception thrown: ' . $exception->getMessage());
|
$this->logger->error('> [MonitorTvEpisodeHandler] ...Exception thrown: ' . $exception->getMessage());
|
||||||
$this->logger->error($exception->getMessage());
|
$this->logger->error($exception->getMessage());
|
||||||
$monitor->setStatus('Active');
|
$monitor->setStatus('Active');
|
||||||
$this->monitorRepository->getEntityManager()->flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$monitor->setLastSearch(new DateTimeImmutable());
|
||||||
|
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||||
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
return new MonitorTvEpisodeResult(
|
return new MonitorTvEpisodeResult(
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
result: [
|
result: [
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace App\Monitor\Action\Handler;
|
namespace App\Monitor\Action\Handler;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||||
use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvSeasonResult;
|
use App\Monitor\Action\Result\MonitorTvSeasonResult;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MediaFiles;
|
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace App\Monitor\Action\Handler;
|
namespace App\Monitor\Action\Handler;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvShowResult;
|
use App\Monitor\Action\Result\MonitorTvShowResult;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Service\MediaFiles;
|
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ class ApiController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/monitor/dispatch', name: 'api_monitor_dispatch', methods: ['GET'])]
|
#[Route('/api/monitor/dispatch', name: 'api_monitor_dispatch', methods: ['GET'])]
|
||||||
public function dispatch(MonitorDispatcher $dispatcher): Response
|
public function dispatch(MonitorDispatcher $dispatcher, Broadcaster $broadcaster): Response
|
||||||
{
|
{
|
||||||
$dispatcher();
|
$dispatcher();
|
||||||
|
|
||||||
|
$broadcaster->alert('Success', 'The monitor job has been dispatched.');
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'status' => 200,
|
'status' => 200,
|
||||||
'message' => 'Manually dispatched MonitorDispatcher'
|
'message' => 'Manually dispatched MonitorDispatcher'
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class Monitor
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?int $searchCount = null;
|
private ?int $searchCount = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private bool $onlyFuture = true;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
private ?\DateTimeInterface $lastSearch = null;
|
private ?\DateTimeInterface $lastSearch = null;
|
||||||
|
|
||||||
@@ -147,6 +150,11 @@ class Monitor
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isOnlyFuture(): bool
|
||||||
|
{
|
||||||
|
return $this->onlyFuture;
|
||||||
|
}
|
||||||
|
|
||||||
public function getLastSearch(): ?\DateTimeInterface
|
public function getLastSearch(): ?\DateTimeInterface
|
||||||
{
|
{
|
||||||
return Carbon::parse($this->lastSearch);
|
return Carbon::parse($this->lastSearch);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Handler;
|
namespace App\Search\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Search\Action\Command\GetMediaInfoCommand;
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
use App\Search\Action\Result\GetMediaInfoResult;
|
use App\Search\Action\Result\GetMediaInfoResult;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
@@ -14,12 +15,19 @@ class GetMediaInfoHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly Tmdb $tmdb,
|
||||||
|
private readonly MediaFiles $mediaFiles
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||||
|
|
||||||
|
if ("tvshows" === $command->mediaType) {
|
||||||
|
foreach ($media->episodes[$command->season] as $key => $episode) {
|
||||||
|
$media->episodes[$command->season][$key]['file'] = $this->mediaFiles->episodeExists($media->title, $command->season, $episode['episode_number']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new GetMediaInfoResult($media, $command->season);
|
return new GetMediaInfoResult($media, $command->season);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Action\Handler;
|
namespace App\Torrentio\Action\Handler;
|
||||||
|
|
||||||
use App\Monitor\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
||||||
use App\Torrentio\Client\Torrentio;
|
use App\Torrentio\Client\Torrentio;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Action\Handler;
|
namespace App\Torrentio\Action\Handler;
|
||||||
|
|
||||||
use App\Monitor\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
|
|||||||
10
src/Twig/Components/ActionButton.php
Normal file
10
src/Twig/Components/ActionButton.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Twig\Components;
|
||||||
|
|
||||||
|
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||||
|
|
||||||
|
#[AsTwigComponent]
|
||||||
|
final class ActionButton
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Twig\Extensions;
|
namespace App\Twig\Extensions;
|
||||||
|
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Monitor\Service\MediaFiles;
|
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
use App\Torrentio\Result\TorrentioResult;
|
|
||||||
use ChrisUllyott\FileSize;
|
use ChrisUllyott\FileSize;
|
||||||
use Tmdb\Model\Tv\Episode;
|
|
||||||
use Twig\Attribute\AsTwigFilter;
|
use Twig\Attribute\AsTwigFilter;
|
||||||
use Twig\Attribute\AsTwigFunction;
|
use Twig\Attribute\AsTwigFunction;
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ class UserPreferences
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $resolution,
|
public readonly ?string $resolution,
|
||||||
public readonly string $codec,
|
public readonly ?string $codec,
|
||||||
public readonly string $language,
|
public readonly ?string $language,
|
||||||
public readonly string $provider,
|
public readonly ?string $provider,
|
||||||
public readonly string $quality,
|
public readonly ?string $quality,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,46 @@
|
|||||||
|
|
||||||
namespace App\User\Dto;
|
namespace App\User\Dto;
|
||||||
|
|
||||||
|
use App\User\Framework\Entity\PreferenceOption;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
class UserPreferencesFactory
|
class UserPreferencesFactory
|
||||||
{
|
{
|
||||||
public static function createFromUser(User $user): UserPreferences
|
/** @param User $user */
|
||||||
|
public static function createFromUser(UserInterface $user): UserPreferences
|
||||||
{
|
{
|
||||||
return new UserPreferences(
|
return new UserPreferences(
|
||||||
resolution: $user->getUserPreference('resolution')->getPreferenceValue(),
|
resolution: self::getNestedValue($user, 'resolution'),
|
||||||
codec: $user->getUserPreference('codec')->getPreferenceValue(),
|
codec: self::getNestedValue($user, 'codec'),
|
||||||
language: $user->getUserPreference('language')->getPreferenceValue(),
|
language: self::getValue($user, 'language'),
|
||||||
provider: $user->getUserPreference('provider')->getPreferenceValue(),
|
provider: self::getValue($user, 'provider'),
|
||||||
quality: $user->getUserPreference('quality')->getPreferenceValue(),
|
quality: self::getValue($user, 'quality'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param User $user */
|
||||||
|
private static function getValue(UserInterface $user, string $preferenceId)
|
||||||
|
{
|
||||||
|
$value = $user->getUserPreference($preferenceId)->getPreferenceValue();
|
||||||
|
if ($value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param User $user */
|
||||||
|
private static function getNestedValue(UserInterface $user, string $preferenceId): ?string
|
||||||
|
{
|
||||||
|
$preference = $user->getUserPreference($preferenceId);
|
||||||
|
if (null === $preference) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $preference->getPreference()
|
||||||
|
->getPreferenceOptions()
|
||||||
|
->filter(fn (PreferenceOption $option) => (string) $option->getId() === $preference->getPreferenceValue())
|
||||||
|
->first()
|
||||||
|
->getValue()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
public function getUserPreference(string $preferenceName): ?UserPreference
|
public function getUserPreference(string $preferenceName): ?UserPreference
|
||||||
{
|
{
|
||||||
foreach ($this->userPreferences as $userPreference) {
|
foreach ($this->userPreferences as $userPreference) {
|
||||||
if ($userPreference->getPreference()->getName() === $preferenceName) {
|
if ($userPreference->getPreference()->getName() === $preferenceName
|
||||||
|
|| $userPreference->getPreference()->getId() === $preferenceName
|
||||||
|
) {
|
||||||
return $userPreference;
|
return $userPreference;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
||||||
<twig:Header />
|
<twig:Header />
|
||||||
<h2 class="px-4 mt-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="px-4 mt-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||||
|
<div class="flex mt-4 gap-2 items-center grow-0 md:px-4">
|
||||||
|
{% block action_buttons %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
templates/components/ActionButton.html.twig
Normal file
13
templates/components/ActionButton.html.twig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<button
|
||||||
|
class="h-6 bg-{{ color|default('orange') }}-500/80 hover:bg-{{ color|default('orange') }}-600/80 px-2 text-{{ text_color|default('white') }} rounded-ms text-sm font-semibold"
|
||||||
|
|
||||||
|
{% if custom_controller|default and custom_action|default %}
|
||||||
|
{{ attributes.defaults(stimulus_controller(custom_controller, custom_controller_vars|default({}))) }}
|
||||||
|
{{ stimulus_action(custom_controller, custom_action|default('default'), custom_action_event|default('click'), custom_action_params|default({})) }}
|
||||||
|
{% else %}
|
||||||
|
{{ attributes.defaults(stimulus_controller('action_button')) }}
|
||||||
|
{{ stimulus_action('action_button', action|default('default')) }}
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{{ text|default('button') }}
|
||||||
|
</button>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<div id="filter" class="flex flex-col gap-4"
|
<div id="filter" class="flex flex-col gap-4"
|
||||||
{{ stimulus_controller('result_filter', {reverseMappedQualities: this.reverseMappedQualities}) }}
|
{{ stimulus_controller('result_filter', {reverseMappedQualities: this.reverseMappedQualities, imdbId: results.media.imdbId}) }}
|
||||||
data-result-filter-media-type-value="{{ results.media.mediaType }}"
|
data-result-filter-media-type-value="{{ results.media.mediaType }}"
|
||||||
data-result-filter-movie-results-outlet=".results"
|
data-result-filter-movie-results-outlet=".results"
|
||||||
data-result-filter-tv-results-outlet=".results"
|
data-result-filter-tv-results-outlet=".results"
|
||||||
data-result-filter-tv-episode-list-outlet=".episode-list"
|
data-result-filter-tv-episode-list-outlet=".episode-list"
|
||||||
data-action="change->result-filter#filter movie-results:optionsLoaded@window->result-filter#loadOptions tv-results:optionsLoaded@window->result-filter#loadOptions"
|
data-action="change->result-filter#filter movie-results:optionsLoaded@window->result-filter#loadOptions tv-results:optionsLoaded@window->result-filter#loadOptions action-button:downloadSeason@window->result-filter#downloadSeason"
|
||||||
>
|
>
|
||||||
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
||||||
<label for="resolution">
|
<label for="resolution">
|
||||||
@@ -94,10 +94,19 @@
|
|||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
<div class="flex flex-row gap-2 justify-end px-8">
|
<div class="flex flex-row gap-2 justify-end px-8">
|
||||||
<button class="px-1.5 py-1 bg-green-600 hover:bg-green-700 rounded-md text-sm"
|
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 bg-green-600 rounded-ms text-sm font-semibold" show_cancel show_submit>
|
||||||
|
Downloading an entire season this way will use the filter from your
|
||||||
|
<a href="{{ path('app_user_preferences') }}" class="text-underline">preferences</a> to choose
|
||||||
|
the appropriate file(s).
|
||||||
|
<br /><br />
|
||||||
|
Do you wish to download <strong>season {{ results.season }}</strong> of "<strong>{{ results.media.title }}</strong>"?
|
||||||
|
</twig:Modal>
|
||||||
|
|
||||||
|
<button class="px-1.5 py-1 bg-green-600 hover:bg-green-700 rounded-ms text-sm font-semibold"
|
||||||
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
||||||
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
||||||
>Download Selected</button>
|
>Download Selected</button>
|
||||||
|
|
||||||
<input type="checkbox" name="selectAll" id="selectAll"
|
<input type="checkbox" name="selectAll" id="selectAll"
|
||||||
{{ stimulus_target('result_filter', 'selectAll') }}
|
{{ stimulus_target('result_filter', 'selectAll') }}
|
||||||
{{ stimulus_action('result_filter', 'selectAllEpisodes', 'change') }}
|
{{ stimulus_action('result_filter', 'selectAllEpisodes', 'change') }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div{{ attributes }} data-controller="dialog" data-action="click->dialog#backdropClose" class="flex flex-row items-center">
|
<div{{ attributes }} data-controller="dialog" data-action="click->dialog#backdropClose" class="modal flex flex-row items-center {{ container_class|default('') }}">
|
||||||
<dialog data-dialog-target="dialog" class="py-3 px-4 w-[30rem] rounded-md">
|
<dialog data-dialog-target="dialog" class="py-3 px-4 w-[30rem] rounded-md">
|
||||||
<h2 class="mb-4 text-2xl font-bold">{{ heading }}</h2>
|
<h2 class="mb-4 text-2xl font-bold">{{ heading }}</h2>
|
||||||
|
|
||||||
@@ -22,5 +22,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<button type="button" data-action="dialog#open">{{ button_text|raw }}</button>
|
<button type="button" class="{{ button_class|default('') }}" data-action="dialog#open">{{ button_text|raw }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,6 +39,31 @@
|
|||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ episode['name'] }}">
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ episode['name'] }}">
|
||||||
{{ episode['air_date']|date(null, 'UTC') }}
|
{{ episode['air_date']|date(null, 'UTC') }}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
{% if episode['file'] != false %}
|
||||||
|
<span data-controller="popover">
|
||||||
|
<template data-popover-target="content">
|
||||||
|
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||||
|
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||||
|
<ul class="list-disc ml-3">
|
||||||
|
<li class="font-normal">{{ episode['file'].realPath|strip_media_path }} — <strong>{{ episode['file'].size|filesize }}</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<small
|
||||||
|
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||||
|
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||||
|
>
|
||||||
|
exists
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if episode['file'] == false %}
|
||||||
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||||
|
missing
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 justify-between">
|
<div class="flex flex-col gap-4 justify-between">
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
{% block title %}Monitors — Torsearch{% endblock %}
|
{% block title %}Monitors — Torsearch{% endblock %}
|
||||||
{% block h2 %}Monitors{% endblock %}
|
{% block h2 %}Monitors{% endblock %}
|
||||||
|
|
||||||
|
{% block action_buttons %}
|
||||||
|
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="px-4 py-2">
|
<div class="px-4 py-2">
|
||||||
<twig:Card title="Active Monitors">
|
<twig:Card title="Active Monitors">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="p-4 flex flex-col grow gap-4">
|
<div class="p-4 flex flex-col grow gap-4">
|
||||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
||||||
<div class="flex flex-row w-full gap-2">
|
<div class="flex flex-row w-full gap-2">
|
||||||
<twig:Card title="" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
||||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
{% if results.media.poster != null %}
|
{% if results.media.poster != null %}
|
||||||
<img class="w-full md:w-40 rounded-lg" src="{{ results.media.poster }}" />
|
<img class="w-full md:w-40 rounded-lg" src="{{ results.media.poster }}" />
|
||||||
@@ -22,87 +22,30 @@
|
|||||||
{{ results.media.title }} - {{ results.media.year }}
|
{{ results.media.title }} - {{ results.media.year }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{# <div data-controller="dropdown" class="relative"#}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
{# {{ stimulus_controller('monitor_button', {#}
|
|
||||||
{# tmdbId: results.media.tmdbId,#}
|
|
||||||
{# imdbId: results.media.imdbId,#}
|
|
||||||
{# title: results.media.title,#}
|
|
||||||
{# })}}#}
|
|
||||||
{# data-monitor-button-result-filter-outlet="#filter"#}
|
|
||||||
{# >#}
|
|
||||||
{# <button type="button" data-action="dropdown#toggle click@window->dropdown#hide"#}
|
|
||||||
{# class="h-8 text-white bg-green-800 bg-opacity-60 font-medium rounded-lg text-sm#}
|
|
||||||
{# px-2 py-1.5 text-center inline-flex items-center hover:bg-green-900 border-2#}
|
|
||||||
{# border-green-500">#}
|
|
||||||
{# Monitor#}
|
|
||||||
{# <svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">#}
|
|
||||||
{# <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" /></svg>#}
|
|
||||||
{# </svg>#}
|
|
||||||
{# </button>#}
|
|
||||||
|
|
||||||
{# <div#}
|
|
||||||
{# data-dropdown-target="menu"#}
|
|
||||||
{# class="hidden transition transform origin-top-right absolute right-0#}
|
|
||||||
{# flex flex-col rounded-md shadow-sm w-44 bg-green-800 border-2 border-green-500 mt-1"#}
|
|
||||||
{# data-transition-enter-from="opacity-0 scale-95"#}
|
|
||||||
{# data-transition-enter-to="opacity-100 scale-100"#}
|
|
||||||
{# data-transition-leave-from="opacity-100 scale-100"#}
|
|
||||||
{# data-transition-leave-to="opacity-0 scale-95"#}
|
|
||||||
{# >#}
|
|
||||||
{# <a href="#"#}
|
|
||||||
{# data-action="dropdown#toggle"#}
|
|
||||||
{# class="backdrop-filter p-2 bg-opacity-100 hover:bg-green-950 rounded-t-md"#}
|
|
||||||
{# >#}
|
|
||||||
{# Entire Series#}
|
|
||||||
{# </a>#}
|
|
||||||
{# <a href="#"#}
|
|
||||||
{# data-action="dropdown#toggle"#}
|
|
||||||
{# class="backdrop-filter p-2 bg-opacity-100 hover:bg-green-950 rounded-b-md"#}
|
|
||||||
{# >#}
|
|
||||||
{# Season#}
|
|
||||||
{# </a>#}
|
|
||||||
{# </div>#}
|
|
||||||
{# </div>#}
|
|
||||||
|
|
||||||
|
|
||||||
<div {{ stimulus_controller('monitor_button', {
|
<div {{ stimulus_controller('monitor_button', {
|
||||||
tmdbId: results.media.tmdbId,
|
tmdbId: results.media.tmdbId,
|
||||||
imdbId: results.media.imdbId,
|
imdbId: results.media.imdbId,
|
||||||
title: results.media.title,
|
title: results.media.title,
|
||||||
})}}
|
})}}
|
||||||
data-monitor-button-result-filter-outlet="#filter"
|
data-monitor-button-result-filter-outlet="#filter"
|
||||||
|
data-monitor-button-dialog-outlet=".monitor-modal"
|
||||||
>
|
>
|
||||||
<button data-monitor-button-target="button" {{ stimulus_action('monitor_button', 'toggle', 'click') }}
|
<twig:Modal
|
||||||
class="h-8 text-white bg-green-800 bg-opacity-60 font-medium rounded-lg text-sm
|
unique_class="monitor-modal"
|
||||||
px-2 py-1.5 text-center inline-flex items-center hover:bg-green-900 border-2
|
button_class="h-8 text-white bg-green-800 bg-opacity-60 font-medium rounded-lg text-sm
|
||||||
border-green-500"
|
px-2 py-1.5 text-center inline-flex items-center hover:bg-green-900 border-2
|
||||||
type="button"
|
border-green-500"
|
||||||
|
container_class="monitor-modal"
|
||||||
|
heading="'Hol Up!" button_text="Monitor" submit_action="{{ stimulus_action('monitor_button', 'monitorSeries', 'click') }}" show_cancel show_submit
|
||||||
>
|
>
|
||||||
Monitor
|
Monitoring a series will continuously search for new episodes and attempt to automatically download them. Your download preferences
|
||||||
<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
will be used to choose the correct file. To stop monitoring for new episodes, delete the monitor.
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
<br /><br />
|
||||||
</svg>
|
Would you like to add a new monitor for "{{ results.media.title }}"?
|
||||||
</button>
|
</twig:Modal>
|
||||||
|
|
||||||
<!-- Dropdown menu -->
|
|
||||||
<div data-monitor-button-target="options"
|
|
||||||
class="absolute mt-1 right-12 z-40 hidden divide-y rounded-md shadow-sm
|
|
||||||
w-44 bg-green-800 backdrop-filter bg-opacity-100 border-2 border-green-500"
|
|
||||||
>
|
|
||||||
<ul class="py-2 text-sm text-gray-100" aria-labelledby="dropdownDefaultButton">
|
|
||||||
<li {{ stimulus_action('monitor_button', 'monitorSeries', 'click') }}>
|
|
||||||
<button class="px-4 py-2 hover:bg-green-950 w-full text-left">
|
|
||||||
Entire Series
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li {{ stimulus_action('monitor_button', 'monitorSeason', 'click') }}>
|
|
||||||
<button class="px-4 py-2 hover:bg-green-950 w-full text-left">
|
|
||||||
Season
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user