Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5729949774 | |||
| e055ed0c15 | |||
| 46d90e800c | |||
| 9fb513bfbd | |||
| 2384bb2414 | |||
| 7c8fa0c439 | |||
| 97aa8d8982 | |||
| a88720fe7e | |||
| 8a12303470 | |||
| 13b9047841 | |||
| 8c0ec98c20 | |||
| 2c9138290a | |||
| c1a6cddb8f |
@@ -51,6 +51,7 @@ export default class extends Controller {
|
|||||||
let selectedCount = 0;
|
let selectedCount = 0;
|
||||||
|
|
||||||
this.options.forEach((option) => {
|
this.options.forEach((option) => {
|
||||||
|
const optionHeader = document.querySelector(`[data-option-id="${option.dataset['localId']}"]`)
|
||||||
const props = {
|
const props = {
|
||||||
"resolution": option.querySelector('#resolution').textContent.trim(),
|
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||||
"codec": option.querySelector('#codec').textContent.trim(),
|
"codec": option.querySelector('#codec').textContent.trim(),
|
||||||
@@ -62,6 +63,8 @@ export default class extends Controller {
|
|||||||
let include = true;
|
let include = true;
|
||||||
option.classList.add('r-tablerow');
|
option.classList.add('r-tablerow');
|
||||||
option.classList.remove('hidden');
|
option.classList.remove('hidden');
|
||||||
|
optionHeader.classList.add('r-tablerow');
|
||||||
|
optionHeader.classList.remove('hidden');
|
||||||
option.querySelector('input[type="checkbox"]').checked = false;
|
option.querySelector('input[type="checkbox"]').checked = false;
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(activeFilter)) {
|
for (let [key, value] of Object.entries(activeFilter)) {
|
||||||
@@ -88,6 +91,8 @@ export default class extends Controller {
|
|||||||
if (false === include) {
|
if (false === include) {
|
||||||
option.classList.remove('r-tablerow');
|
option.classList.remove('r-tablerow');
|
||||||
option.classList.add('hidden');
|
option.classList.add('hidden');
|
||||||
|
optionHeader.classList.remove('r-tablerow');
|
||||||
|
optionHeader.classList.add('hidden');
|
||||||
} else if (true === firstIncluded) {
|
} else if (true === firstIncluded) {
|
||||||
count = 1;
|
count = 1;
|
||||||
selectedCount = selectedCount + 1;
|
selectedCount = selectedCount + 1;
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export default class extends Controller {
|
|||||||
let selectedCount = 0;
|
let selectedCount = 0;
|
||||||
|
|
||||||
this.options.forEach((option) => {
|
this.options.forEach((option) => {
|
||||||
|
const optionHeader = document.querySelector(`[data-option-id="${option.dataset['localId']}"]`)
|
||||||
const props = {
|
const props = {
|
||||||
"resolution": option.querySelector('#resolution').textContent.trim(),
|
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||||
"codec": option.querySelector('#codec').textContent.trim(),
|
"codec": option.querySelector('#codec').textContent.trim(),
|
||||||
@@ -138,6 +139,8 @@ export default class extends Controller {
|
|||||||
let include = true;
|
let include = true;
|
||||||
option.classList.add('r-tablerow');
|
option.classList.add('r-tablerow');
|
||||||
option.classList.remove('hidden');
|
option.classList.remove('hidden');
|
||||||
|
optionHeader.classList.add('r-tablerow');
|
||||||
|
optionHeader.classList.remove('hidden');
|
||||||
option.querySelector('input[type="checkbox"]').checked = false;
|
option.querySelector('input[type="checkbox"]').checked = false;
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(activeFilter)) {
|
for (let [key, value] of Object.entries(activeFilter)) {
|
||||||
@@ -164,6 +167,8 @@ export default class extends Controller {
|
|||||||
if (false === include) {
|
if (false === include) {
|
||||||
option.classList.remove('r-tablerow');
|
option.classList.remove('r-tablerow');
|
||||||
option.classList.add('hidden');
|
option.classList.add('hidden');
|
||||||
|
optionHeader.classList.remove('r-tablerow');
|
||||||
|
optionHeader.classList.add('hidden');
|
||||||
} else if (true === firstIncluded) {
|
} else if (true === firstIncluded) {
|
||||||
count = 1;
|
count = 1;
|
||||||
selectedCount = selectedCount + 1;
|
selectedCount = selectedCount + 1;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class ResultFactory
|
|||||||
string $bingeGroup = "-"
|
string $bingeGroup = "-"
|
||||||
) {
|
) {
|
||||||
$ptn = (object) (new PTN())->parse($title);
|
$ptn = (object) (new PTN())->parse($title);
|
||||||
// dump($ptn);
|
|
||||||
return new TorrentioResult(
|
return new TorrentioResult(
|
||||||
self::trimTitle($title),
|
self::trimTitle($title),
|
||||||
urldecode($url),
|
urldecode($url),
|
||||||
@@ -40,7 +39,8 @@ class ResultFactory
|
|||||||
$ptn->episode ?? "-",
|
$ptn->episode ?? "-",
|
||||||
self::setLanguages($title),
|
self::setLanguages($title),
|
||||||
self::setLanguageFlags($title),
|
self::setLanguageFlags($title),
|
||||||
false
|
false,
|
||||||
|
uniqid()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ class TorrentioResult
|
|||||||
public ?array $languages = [],
|
public ?array $languages = [],
|
||||||
public ?string $languageFlags = "-",
|
public ?string $languageFlags = "-",
|
||||||
public ?bool $selected = false,
|
public ?bool $selected = false,
|
||||||
|
public ?string $localId = "-"
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
*/
|
*/
|
||||||
public function getUserIdentifier(): string
|
public function getUserIdentifier(): string
|
||||||
{
|
{
|
||||||
return (string) $this->username ?? $this->email;
|
return (string) $this->email;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<button
|
<button
|
||||||
class="h-8 bg-{{ color|default('orange') }}-500/80 hover:bg-{{ color|default('orange') }}-600/80 px-2 text-{{ text_color|default('white') }} rounded text-sm font-semibold"
|
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"
|
||||||
{{ attributes.defaults(stimulus_controller('action_button')) }}
|
|
||||||
{{ stimulus_action('action_button', action|default('default')) }}
|
{% 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') }}
|
{{ text|default('button') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div{{ attributes.defaults(stimulus_controller('download_list')) }} class="min-w-48" >
|
<div{{ attributes.defaults(stimulus_controller('download_list')) }} class="min-w-48 overflow-scroll" >
|
||||||
{% set table_body_id = (type == "complete") ? "complete_downloads" : "active_downloads" %}
|
{% set table_body_id = (type == "complete") ? "complete_downloads" : "active_downloads" %}
|
||||||
|
|
||||||
{% if this.isWidget == false %}
|
{% if this.isWidget == false %}
|
||||||
|
|||||||
@@ -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.defaults(stimulus_controller('monitor_list')) }}>
|
<div{{ attributes.defaults(stimulus_controller('monitor_list')) }} class="overflow-scroll">
|
||||||
{% if this.isWidget == false %}
|
{% if this.isWidget == false %}
|
||||||
<div class="flex flex-row mb-2 justify-end">
|
<div class="flex flex-row mb-2 justify-end">
|
||||||
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
|
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% if results.file != false %}
|
{% if results.file != false %}
|
||||||
<div class="p-3 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
<div class="p-3 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 movie:</p>
|
<p class="font-bold text-sm text-left">Existing file(s) for this movie:</p>
|
||||||
<ul class="list-disc ml-3">
|
<ul class="list-disc ml-3 overflow-scroll">
|
||||||
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
>
|
>
|
||||||
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
|
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
|
||||||
{% for result in results.results %}
|
{% for result in results.results %}
|
||||||
<tr class="dark:bg-stone-600 overflow-hidden flex flex-col md:flex-col flex-no wrap md:table-row border-b border-gray-500">
|
<tr data-option-id="{{ result.localId }}" class="dark:bg-stone-600 overflow-hidden flex flex-col md:flex-col flex-no wrap md:table-row border-b border-gray-500">
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
Size
|
Size
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="flex-1 sm:flex-none">
|
<tbody class="flex-1 sm:flex-none">
|
||||||
{% for result in results.results %}
|
{% for result in results.results %}
|
||||||
<tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow border-b border-gray-500" data-provider="{{ result.provider }}" data-quality="{{ result.quality }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
|
<tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow border-b border-gray-500" data-local-id="{{ result.localId }}" data-provider="{{ result.provider }}" data-quality="{{ result.quality }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
|
||||||
<td id="size" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
<td id="size" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||||
{{ result.size }}
|
{{ result.size }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user