Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a88720fe7e | |||
| 8a12303470 | |||
| 13b9047841 | |||
| 8c0ec98c20 | |||
| 2c9138290a |
@@ -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()) {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
47
migrations/Version20250708033046.php
Normal file
47
migrations/Version20250708033046.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class 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'
|
||||||
|
DROP TABLE sessions
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
||||||
|
SQL);
|
||||||
|
$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'
|
||||||
|
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP only_future
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
||||||
|
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();
|
||||||
|
|||||||
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',
|
||||||
|
) {}
|
||||||
|
}
|
||||||
111
src/Download/Action/Handler/DownloadSeasonHandler.php
Normal file
111
src/Download/Action/Handler/DownloadSeasonHandler.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Handler;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
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\Download\Framework\Repository\DownloadRepository;
|
||||||
|
use App\Download\Downloader\DownloaderInterface;
|
||||||
|
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||||
|
use App\Monitor\Action\Handler\MonitorTvEpisodeHandler;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
|
use App\Monitor\Service\MediaFiles;
|
||||||
|
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,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
94
src/Download/DownloadOptionEvaluator.php
Normal file
94
src/Download/DownloadOptionEvaluator.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
|
use App\Torrentio\Result\TorrentioResult;
|
||||||
|
use App\User\Dto\UserPreferences;
|
||||||
|
|
||||||
|
class DownloadOptionEvaluator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Monitor $monitor
|
||||||
|
* @param TorrentioResult[] $results
|
||||||
|
* @return TorrentioResult|null
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function evaluateOptions(array $results, UserPreferences $userPreferences): ?TorrentioResult
|
||||||
|
{
|
||||||
|
$sizeLow = 000;
|
||||||
|
$sizeHigh = 4096;
|
||||||
|
|
||||||
|
$bestMatches = [];
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
foreach ($results as $result) {
|
||||||
|
if (!in_array($userPreferences->language, $result->languages)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result->resolution === $userPreferences->resolution
|
||||||
|
&& $result->codec === $userPreferences->codec
|
||||||
|
) {
|
||||||
|
$bestMatches[] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userPreferences->resolution === '2160p'
|
||||||
|
&& $userPreferences->codec === $result->codec
|
||||||
|
&& $result->resolution === '1080p'
|
||||||
|
) {
|
||||||
|
$matches[] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userPreferences->codec === 'h264'
|
||||||
|
&& $userPreferences->resolution === $result->resolution
|
||||||
|
&& $result->codec === 'h265'
|
||||||
|
) {
|
||||||
|
$matches[] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($userPreferences->codec === null )
|
||||||
|
&& ($userPreferences->resolution === null )) {
|
||||||
|
$matches[] = $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sizeMatches = [];
|
||||||
|
|
||||||
|
foreach ($bestMatches as $result) {
|
||||||
|
if (str_contains($result->size, 'GB')) {
|
||||||
|
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
||||||
|
} else {
|
||||||
|
$size = (int) trim(str_replace('MB', '', $result->size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size > $sizeLow && $size < $sizeHigh) {
|
||||||
|
$sizeMatches[] = $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sizeMatches)) {
|
||||||
|
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($matches as $result) {
|
||||||
|
$size = 0;
|
||||||
|
if (str_contains($result->size, 'GB')) {
|
||||||
|
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
||||||
|
} else {
|
||||||
|
$size = (int) trim(str_replace('MB', '', $result->size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size > $sizeLow && $size < $sizeHigh) {
|
||||||
|
$sizeMatches[] = $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sizeMatches)) {
|
||||||
|
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<button
|
<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 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,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') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user