Compare commits
1 Commits
main
...
dev-simple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39cb6e9bd |
@@ -102,6 +102,14 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download() {
|
download() {
|
||||||
|
const preferencesForm = document.querySelector('[name="user_media_preferences_form"]');
|
||||||
|
const preferences = {
|
||||||
|
resolution: preferencesForm.querySelector('[id="user_media_preferences_form_resolution"]').value,
|
||||||
|
codec: preferencesForm.querySelector('[id="user_media_preferences_form_codec"]').value,
|
||||||
|
language: preferencesForm.querySelector('[id="user_media_preferences_form_language"]').value,
|
||||||
|
quality: preferencesForm.querySelector('[id="user_media_preferences_form_quality"]').value,
|
||||||
|
provider: preferencesForm.querySelector('[id="user_media_preferences_form_provider"]').value,
|
||||||
|
}
|
||||||
fetch('/api/download', {
|
fetch('/api/download', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -109,12 +117,11 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
|
|||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: this.url,
|
|
||||||
title: this.mediaTitle,
|
|
||||||
filename: this.filename,
|
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
imdbId: this.imdbId,
|
imdbId: this.imdbId,
|
||||||
episodeId: this.episodeId
|
season: this.season,
|
||||||
|
episode: this.episode,
|
||||||
|
filter: preferences,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download() {
|
download() {
|
||||||
|
console.log(new FormData(document.querySelector('[name="user_media_preferences_form"]')).values());
|
||||||
fetch('/api/download', {
|
fetch('/api/download', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -28,7 +29,8 @@ export default class extends Controller {
|
|||||||
filename: this.filenameValue,
|
filename: this.filenameValue,
|
||||||
mediaType: this.mediaTypeValue,
|
mediaType: this.mediaTypeValue,
|
||||||
imdbId: this.imdbIdValue,
|
imdbId: this.imdbIdValue,
|
||||||
episodeId: this.episodeIdValue
|
episodeId: this.episodeIdValue,
|
||||||
|
filter: new FormData(document.querySelector('[name="user_media_preferences_form"]')).values()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ services:
|
|||||||
App\:
|
App\:
|
||||||
resource: '../src/'
|
resource: '../src/'
|
||||||
exclude:
|
exclude:
|
||||||
|
- '../src/Library/Dto'
|
||||||
- '../src/DependencyInjection/'
|
- '../src/DependencyInjection/'
|
||||||
- '../src/Entity/'
|
- '../src/Entity/'
|
||||||
- '../src/Kernel.php'
|
- '../src/Kernel.php'
|
||||||
|
|||||||
35
migrations/Version20260321191300.php
Normal file
35
migrations/Version20260321191300.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 Version20260321191300 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 download CHANGE url url VARCHAR(1024) DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE download CHANGE url url VARCHAR(1024) NOT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,13 +113,14 @@ class MediaFiles
|
|||||||
return Map::from($results);
|
return Map::from($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createMovieDirectory(string $title, string|int $year): string
|
public function createMovieDirectory(string $title, string|int $year, string $imdbId): string
|
||||||
{
|
{
|
||||||
$path = sprintf(
|
$path = sprintf(
|
||||||
'%s' . DIRECTORY_SEPARATOR . '%s (%s)',
|
'%s' . DIRECTORY_SEPARATOR . '%s (%s) [imdbid-%s]',
|
||||||
$this->moviesPath,
|
$this->moviesPath,
|
||||||
$title,
|
$title,
|
||||||
$year
|
$year,
|
||||||
|
$imdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (false === $this->filesystem->exists($path)) {
|
if (false === $this->filesystem->exists($path)) {
|
||||||
@@ -129,13 +130,15 @@ class MediaFiles
|
|||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createTvShowDirectory(string $title, string|int $year): string
|
public function createTvShowDirectory(string $title, string|int $year, string|int $season, string|int $episode, string $imdbId): string
|
||||||
{
|
{
|
||||||
$path = sprintf(
|
$path = sprintf(
|
||||||
'%s' . DIRECTORY_SEPARATOR . '%s (%s)',
|
'%s' . DIRECTORY_SEPARATOR . '%s (%s) [imdbid-%s]' . DIRECTORY_SEPARATOR . 'Season %s',
|
||||||
$this->tvShowsPath,
|
$this->tvShowsPath,
|
||||||
$title,
|
$title,
|
||||||
$year
|
$year,
|
||||||
|
$imdbId,
|
||||||
|
str_pad($season, 2, '0', STR_PAD_LEFT),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (false === $this->filesystem->exists($path)) {
|
if (false === $this->filesystem->exists($path)) {
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
class DownloadMediaCommand implements CommandInterface
|
class DownloadMediaCommand implements CommandInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $url,
|
|
||||||
public string $title,
|
|
||||||
public string $filename,
|
|
||||||
public string $mediaType,
|
|
||||||
public string $imdbId,
|
public string $imdbId,
|
||||||
public int $userId,
|
public string $mediaType,
|
||||||
public ?int $downloadId = null,
|
public int|string|null $season = null,
|
||||||
|
public int|string|null $episode = null,
|
||||||
|
public string|null $url = null,
|
||||||
|
public array|null $filter = null,
|
||||||
|
public int|null $downloadId = null,
|
||||||
|
public int|null $userId = null,
|
||||||
public ?string $mercureAlertTopic = null,
|
public ?string $mercureAlertTopic = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,26 @@ namespace App\Download\Action\Handler;
|
|||||||
|
|
||||||
use App\Base\Enum\MediaType;
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
|
use App\Base\Util\EpisodeId;
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Download\Action\Result\DownloadMediaResult;
|
use App\Download\Action\Result\DownloadMediaResult;
|
||||||
use App\Download\DownloadEvents;
|
use App\Download\DownloadEvents;
|
||||||
|
use App\Download\DownloadOptionEvaluator;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Download\Downloader\DownloaderInterface;
|
use App\Download\Downloader\DownloaderInterface;
|
||||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||||
|
use App\Library\Dto\MediaFileDto;
|
||||||
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
|
use App\Search\Action\Handler\GetMediaInfoHandler;
|
||||||
|
use App\Search\Action\Result\GetMediaInfoResult;
|
||||||
|
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||||
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
|
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||||
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
|
use App\Torrentio\Result\TorrentioResult;
|
||||||
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
@@ -18,19 +31,63 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
|
|
||||||
/** @implements HandlerInterface<DownloadMediaCommand, DownloadMediaResult> */
|
/** @implements HandlerInterface<DownloadMediaCommand, DownloadMediaResult> */
|
||||||
readonly class DownloadMediaHandler implements HandlerInterface
|
readonly class DownloadMediaHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private MessageBusInterface $bus,
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
private DownloaderInterface $downloader,
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
private DownloadRepository $downloadRepository,
|
private GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||||
private UserRepository $userRepository, private Broadcaster $broadcaster,
|
private GetMediaInfoHandler $getMediaInfoHandler,
|
||||||
|
private MessageBusInterface $bus,
|
||||||
|
private DownloaderInterface $downloader,
|
||||||
|
private DownloadRepository $downloadRepository,
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
private Broadcaster $broadcaster,
|
||||||
|
private MediaFiles $mediaFiles,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$user = $this->userRepository->find($command->userId);
|
$user = $this->userRepository->find($command->userId);
|
||||||
|
/** @var \App\Download\Framework\Entity\Download $download */
|
||||||
|
$download = $this->downloadRepository->find($command->downloadId);
|
||||||
|
$media = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand(
|
||||||
|
$command->imdbId,
|
||||||
|
$command->mediaType,
|
||||||
|
$command->season,
|
||||||
|
$command->episode,
|
||||||
|
));
|
||||||
|
$downloadOptions = match ($command->mediaType) {
|
||||||
|
MediaType::Movie->value => $this->getMovieOptionsHandler->handle(
|
||||||
|
new GetMovieOptionsCommand(
|
||||||
|
$media->media->tmdbId,
|
||||||
|
$media->media->imdbId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
MediaType::TvShow->value => $this->getTvShowOptionsHandler->handle(
|
||||||
|
new GetTvShowOptionsCommand(
|
||||||
|
$media->media->tmdbId,
|
||||||
|
$media->media->imdbId,
|
||||||
|
$command->season,
|
||||||
|
$command->episode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
$filter = $command->filter !== null
|
||||||
|
? UserPreferencesFactory::createFromArray($command->filter)
|
||||||
|
: UserPreferencesFactory::createFromUser($user);
|
||||||
|
|
||||||
|
$matchingOption = $this->downloadOptionEvaluator->evaluateOptions($downloadOptions->results, $filter);
|
||||||
|
|
||||||
|
$download->setUrl($matchingOption->url);
|
||||||
|
$download->setTitle($media->media->title);
|
||||||
|
$download->setFileName(
|
||||||
|
$this->getFilename(MediaType::from($command->mediaType), $media, $matchingOption)
|
||||||
|
);
|
||||||
|
|
||||||
$this->bus->dispatch(new AddEventLogCommand(
|
$this->bus->dispatch(new AddEventLogCommand(
|
||||||
$user,
|
$user,
|
||||||
DownloadEvents::DOWNLOAD_STARTED->type(),
|
DownloadEvents::DOWNLOAD_STARTED->type(),
|
||||||
@@ -38,20 +95,6 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
|||||||
(array) $command
|
(array) $command
|
||||||
));
|
));
|
||||||
|
|
||||||
if (null === $command->downloadId) {
|
|
||||||
$download = $this->downloadRepository->insert(
|
|
||||||
$user,
|
|
||||||
$command->url,
|
|
||||||
$command->title,
|
|
||||||
$command->filename,
|
|
||||||
$command->imdbId,
|
|
||||||
$command->mediaType,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$download = $this->downloadRepository->find($command->downloadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->validateDownloadUrl($download->getUrl());
|
$this->validateDownloadUrl($download->getUrl());
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
@@ -69,8 +112,8 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
|||||||
|
|
||||||
$this->downloader->download(
|
$this->downloader->download(
|
||||||
$command->mediaType,
|
$command->mediaType,
|
||||||
$command->title,
|
$download->getUrl(),
|
||||||
$command->url,
|
$this->getFilepath(MediaType::from($command->mediaType), $media, $matchingOption),
|
||||||
$download->getId()
|
$download->getId()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,6 +134,54 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
|||||||
return new DownloadMediaResult(200, "Success.");
|
return new DownloadMediaResult(200, "Success.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getFilepath(MediaType $mediaType, GetMediaInfoResult $media, TorrentioResult $option): ?string
|
||||||
|
{
|
||||||
|
return match ($mediaType) {
|
||||||
|
MediaType::Movie => $this->mediaFiles->createMovieDirectory(
|
||||||
|
$media->media->title,
|
||||||
|
$media->media->year,
|
||||||
|
$media->media->imdbId,
|
||||||
|
),
|
||||||
|
MediaType::TvShow => $this->mediaFiles->createTvShowDirectory(
|
||||||
|
$media->media->title,
|
||||||
|
$media->media->year,
|
||||||
|
$media->season,
|
||||||
|
$media->episode,
|
||||||
|
$media->media->imdbId,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFilename(MediaType $mediaType, GetMediaInfoResult $media, TorrentioResult $option): ?string
|
||||||
|
{
|
||||||
|
$fileType = $option->ptn->container;
|
||||||
|
return (match ($mediaType) {
|
||||||
|
MediaType::Movie => function () use ($media, $fileType) {
|
||||||
|
$template = "%s (%s) [imdbid-%s].%s";
|
||||||
|
return sprintf(
|
||||||
|
$template,
|
||||||
|
$media->media->title,
|
||||||
|
$media->media->year,
|
||||||
|
$media->media->imdbId,
|
||||||
|
$fileType
|
||||||
|
);
|
||||||
|
},
|
||||||
|
MediaType::TvShow => function () use ($media, $fileType) {
|
||||||
|
$template = "%s %s.%s";
|
||||||
|
$episodeId = EpisodeId::fromSeasonEpisodeNumbers(
|
||||||
|
$media->season,
|
||||||
|
$media->episode,
|
||||||
|
);
|
||||||
|
return sprintf(
|
||||||
|
$template,
|
||||||
|
$media->media->title,
|
||||||
|
$episodeId,
|
||||||
|
$fileType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
public function validateDownloadUrl(string $downloadUrl)
|
public function validateDownloadUrl(string $downloadUrl)
|
||||||
{
|
{
|
||||||
$badFileSizes = [
|
$badFileSizes = [
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
namespace App\Download\Action\Input;
|
namespace App\Download\Action\Input;
|
||||||
|
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\PropertyIgnored;
|
||||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\InputInterface;
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/** @implements InputInterface<DownloadMediaInput> */
|
/** @implements InputInterface<DownloadMediaInput> */
|
||||||
class DownloadMediaInput implements InputInterface
|
class DownloadMediaInput implements InputInterface
|
||||||
@@ -13,39 +16,60 @@ class DownloadMediaInput implements InputInterface
|
|||||||
public ?string $mercureAlertTopic = null;
|
public ?string $mercureAlertTopic = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[SourceRequest('url')]
|
#[SourceRequest('imdbId')]
|
||||||
public string $url,
|
public string $imdbId,
|
||||||
|
|
||||||
#[SourceRequest('title')]
|
|
||||||
public string $title,
|
|
||||||
|
|
||||||
#[SourceRequest('filename')]
|
|
||||||
public string $filename,
|
|
||||||
|
|
||||||
#[SourceRequest('mediaType')]
|
#[SourceRequest('mediaType')]
|
||||||
public string $mediaType,
|
public string $mediaType,
|
||||||
|
|
||||||
#[SourceRequest('imdbId')]
|
#[SourceRequest('season', nullify: true)]
|
||||||
public string $imdbId,
|
public int|string|null $season = null,
|
||||||
|
|
||||||
#[SourceRequest('episodeId', nullify: true)]
|
#[SourceRequest('episode', nullify: true)]
|
||||||
public ?string $episodeId = null,
|
public int|string|null $episode = null,
|
||||||
|
|
||||||
public ?int $userId = null,
|
#[SourceRequest('url', nullify: true)]
|
||||||
|
public string|null $url = null,
|
||||||
|
|
||||||
public ?int $downloadId = null,
|
#[SourceRequest('filter', nullify: true)]
|
||||||
|
public array|null $filter = null,
|
||||||
|
|
||||||
|
#[PropertyIgnored]
|
||||||
|
public int|null $downloadId = null,
|
||||||
|
|
||||||
|
#[PropertyIgnored]
|
||||||
|
public int|null $userId = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function setUserId(int $userId): static
|
||||||
|
{
|
||||||
|
$this->userId = $userId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDownloadId(int $downloadId): static
|
||||||
|
{
|
||||||
|
$this->downloadId = $downloadId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMercureAlertTopic(string $mercureAlertTopic): static
|
||||||
|
{
|
||||||
|
$this->mercureAlertTopic = $mercureAlertTopic;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function toCommand(): CommandInterface
|
public function toCommand(): CommandInterface
|
||||||
{
|
{
|
||||||
return new DownloadMediaCommand(
|
return new DownloadMediaCommand(
|
||||||
$this->url,
|
|
||||||
$this->title,
|
|
||||||
$this->filename,
|
|
||||||
$this->mediaType,
|
|
||||||
$this->imdbId,
|
$this->imdbId,
|
||||||
$this->userId,
|
$this->mediaType,
|
||||||
|
$this->season,
|
||||||
|
$this->episode,
|
||||||
|
$this->url,
|
||||||
|
$this->filter,
|
||||||
$this->downloadId,
|
$this->downloadId,
|
||||||
|
$this->userId,
|
||||||
$this->mercureAlertTopic,
|
$this->mercureAlertTopic,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ class DownloadOptionEvaluator
|
|||||||
// return false;
|
// return false;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
if (false === $this->validateDownloadUrl($result->url)) {
|
// if (false === $this->validateDownloadUrl($result->url)) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dd($filter);
|
||||||
|
|
||||||
if ($matches->count() > 0) {
|
if ($matches->count() > 0) {
|
||||||
return Map::from($matches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
return Map::from($matches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ interface DownloaderInterface
|
|||||||
* @return void
|
* @return void
|
||||||
* Downloads the requested file.
|
* Downloads the requested file.
|
||||||
*/
|
*/
|
||||||
public function download(string $mediaType, string $title, string $url, ?int $downloadId): void;
|
public function download(string $mediaType, string $url, string $downloadPath, ?int $downloadId): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,24 +24,19 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MediaFiles $mediaFiles,
|
|
||||||
private CacheInterface $cache,
|
private CacheInterface $cache,
|
||||||
private readonly Broadcaster $broadcaster,
|
private readonly Broadcaster $broadcaster,
|
||||||
private readonly GetMediaInfoHandler $getMediaInfoHandler,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function download(string $mediaType, string $title, string $url, ?int $downloadId): void
|
public function download(string $mediaType, string $url, string $downloadPath, ?int $downloadId): void
|
||||||
{
|
{
|
||||||
/** @var Download $downloadEntity */
|
/** @var Download $downloadEntity */
|
||||||
$downloadEntity = $this->entityManager->getRepository(Download::class)->find($downloadId);
|
$downloadEntity = $this->entityManager->getRepository(Download::class)->find($downloadId);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
$downloadPreferences = $downloadEntity->getUser()->getDownloadPreferences();
|
|
||||||
$path = $this->getDownloadPath($mediaType, $title, $downloadEntity->getImdbId(), $downloadPreferences);
|
|
||||||
|
|
||||||
$processArgs = ['wget', '-O', $downloadEntity->getFilename(), $url];
|
$processArgs = ['wget', '-O', $downloadEntity->getFilename(), $url];
|
||||||
|
|
||||||
if ($downloadEntity->getStatus() === 'Paused') {
|
if ($downloadEntity->getStatus() === 'Paused') {
|
||||||
@@ -51,13 +46,9 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
$downloadEntity->setProgress(0);
|
$downloadEntity->setProgress(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite(STDOUT, implode(" ", $processArgs));
|
$process = (new Process($processArgs))->setWorkingDirectory($downloadPath);
|
||||||
|
|
||||||
$process = (new Process($processArgs))->setWorkingDirectory($path);
|
|
||||||
|
|
||||||
$process->setTimeout(1800); // 30 min
|
$process->setTimeout(1800); // 30 min
|
||||||
$process->setIdleTimeout(600); // 10 min
|
$process->setIdleTimeout(600); // 10 min
|
||||||
|
|
||||||
$process->start();
|
$process->start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -96,7 +87,7 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
} catch (ProcessFailedException $exception) {
|
} catch (ProcessFailedException $exception) {
|
||||||
$downloadEntity->setStatus('Failed');
|
$downloadEntity->setStatus('Failed');
|
||||||
$this->bus->dispatch(new AddEventLogCommand(
|
$this->bus->dispatch(new AddEventLogCommand(
|
||||||
$downloadEntity->getUser()->getId(),
|
$downloadEntity->getUser(),
|
||||||
DownloadEvents::DOWNLOAD_ERROR->type(),
|
DownloadEvents::DOWNLOAD_ERROR->type(),
|
||||||
DownloadEvents::DOWNLOAD_ERROR->message() . ': ' . $exception->getMessage(),
|
DownloadEvents::DOWNLOAD_ERROR->message() . ': ' . $exception->getMessage(),
|
||||||
(array) $downloadEntity
|
(array) $downloadEntity
|
||||||
@@ -106,27 +97,6 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDownloadPath(string $mediaType, string $title, string $imdbId, array $downloadPreferences): string
|
|
||||||
{
|
|
||||||
$mediaInfo = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand(
|
|
||||||
$imdbId,
|
|
||||||
$mediaType,
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($mediaType === 'movies') {
|
|
||||||
if ((bool) $downloadPreferences['movie_folder']->getPreferenceValue() === true) {
|
|
||||||
return $this->mediaFiles->createMovieDirectory($title, $mediaInfo->media->year);
|
|
||||||
}
|
|
||||||
return $this->mediaFiles->getMoviesPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mediaType === 'tvshows') {
|
|
||||||
return $this->mediaFiles->createTvShowDirectory($title, $mediaInfo->media->year);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \Exception("There is no download path for media type: $mediaType");
|
|
||||||
}
|
|
||||||
|
|
||||||
private function alertComplete(Download $download): void
|
private function alertComplete(Download $download): void
|
||||||
{
|
{
|
||||||
if ("tvshows" === $download->getMediaType()) {
|
if ("tvshows" === $download->getMediaType()) {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Download\Downloader;
|
|
||||||
|
|
||||||
use App\Message\DownloadMessage;
|
|
||||||
use App\Message\DownloadMovieMessage;
|
|
||||||
use App\Message\DownloadTvShowMessage;
|
|
||||||
|
|
||||||
class WgetDownloader implements DownloaderInterface
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
* SSHs into the NAS and performs the download.
|
|
||||||
* This way retains the fast DL speed on the NAS.
|
|
||||||
*/
|
|
||||||
public function download(string $baseDir, string $title, string $url, ?int $downloadId): void
|
|
||||||
{
|
|
||||||
// SSHs into the NAS, cds into movies dir, makes new dir based on filename, cds into that dir, downloads movie
|
|
||||||
system(sprintf(
|
|
||||||
'sh /var/www/bash/app/wget_download.sh "%s" "%s" "%s"',
|
|
||||||
$baseDir,
|
|
||||||
$title,
|
|
||||||
$url
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ namespace App\Download\Framework\Controller;
|
|||||||
|
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\Download\Action\Handler\DeleteDownloadHandler;
|
use App\Download\Action\Handler\DeleteDownloadHandler;
|
||||||
|
use App\Download\Action\Handler\DownloadMediaHandler;
|
||||||
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;
|
||||||
@@ -12,6 +13,7 @@ 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\DownloadEvents;
|
use App\Download\DownloadEvents;
|
||||||
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -31,19 +33,19 @@ class ApiController extends AbstractController
|
|||||||
#[Route('/api/download', name: 'api_download', methods: ['POST'])]
|
#[Route('/api/download', name: 'api_download', methods: ['POST'])]
|
||||||
public function download(
|
public function download(
|
||||||
DownloadMediaInput $input,
|
DownloadMediaInput $input,
|
||||||
|
DownloadMediaHandler $handler,
|
||||||
): Response {
|
): Response {
|
||||||
$download = $this->downloadRepository->insert(
|
$download = $this->downloadRepository->insertNew(
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
$input->url,
|
|
||||||
$input->title,
|
|
||||||
$input->filename,
|
|
||||||
$input->imdbId,
|
$input->imdbId,
|
||||||
$input->mediaType,
|
$input->mediaType,
|
||||||
$input->episodeId,
|
$input->season,
|
||||||
|
$input->episode,
|
||||||
);
|
);
|
||||||
$input->downloadId = $download->getId();
|
$input->setDownloadId($download->getId());
|
||||||
$input->userId = $this->getUser()->getId();
|
$input->setUserId($this->getUser()->getId());
|
||||||
$input->mercureAlertTopic = $this->requestStack->getSession()->get('mercure_alert_topic');
|
$input->setMercureAlertTopic($this->requestStack->getSession()->get('mercure_alert_topic'));
|
||||||
|
$input->toCommand();
|
||||||
|
|
||||||
$this->bus->dispatch(new AddEventLogCommand(
|
$this->bus->dispatch(new AddEventLogCommand(
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
@@ -60,7 +62,7 @@ class ApiController extends AbstractController
|
|||||||
|
|
||||||
$this->broadcaster->alert(
|
$this->broadcaster->alert(
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: "$input->title added to Queue."
|
message: "Added to Queue."
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->json(['status' => 200, 'message' => 'Added to Queue']);
|
return $this->json(['status' => 200, 'message' => 'Added to Queue']);
|
||||||
@@ -121,4 +123,22 @@ class ApiController extends AbstractController
|
|||||||
|
|
||||||
return $this->json(['status' => 200, 'message' => 'Download Resumed']);
|
return $this->json(['status' => 200, 'message' => 'Download Resumed']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/download', name: 'api_get_downloads', methods: ['GET'])]
|
||||||
|
public function getDownloads(DownloadRepository $repository): Response
|
||||||
|
{
|
||||||
|
$downloads = $repository->findBy(['user' => $this->getUser()]);
|
||||||
|
|
||||||
|
return $this->json(['status' => 200, 'message' => 'Success', 'data' => ['downloads' => $downloads]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/download/{id}', name: 'api_get_download', methods: ['GET'])]
|
||||||
|
public function getDownload(Download $download): Response
|
||||||
|
{
|
||||||
|
if ($download->getUser() === $this->getUser()) {
|
||||||
|
return $this->json(['status' => 200, 'message' => 'Success', 'data' => ['download' => $download]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json(['status' => 404, 'message' => 'Success'], 404);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Download
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $title = null;
|
private ?string $title = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 1024)]
|
#[ORM\Column(length: 1024, nullable: true)]
|
||||||
private ?string $url = null;
|
private ?string $url = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 1024, nullable: true)]
|
#[ORM\Column(length: 1024, nullable: true)]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Download\Framework\Repository;
|
namespace App\Download\Framework\Repository;
|
||||||
|
|
||||||
|
use App\Base\Util\EpisodeId;
|
||||||
use App\Base\Util\Paginator;
|
use App\Base\Util\Paginator;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
@@ -56,6 +57,34 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function insertNew(
|
||||||
|
UserInterface $user,
|
||||||
|
string $imdbId,
|
||||||
|
string $mediaType,
|
||||||
|
int|null $season = null,
|
||||||
|
int|null $episode = null,
|
||||||
|
string $status = 'New'
|
||||||
|
): Download {
|
||||||
|
/** @var User $user */
|
||||||
|
$download = (new Download())
|
||||||
|
->setUser($user)
|
||||||
|
->setImdbId($imdbId)
|
||||||
|
->setMediaType($mediaType)
|
||||||
|
->setProgress(0)
|
||||||
|
->setStatus($status);
|
||||||
|
|
||||||
|
if (null !== $season && null !== $episode) {
|
||||||
|
$download->setEpisodeId(
|
||||||
|
EpisodeId::fromSeasonEpisodeNumbers($season, $episode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getEntityManager()->persist($download);
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
return $download;
|
||||||
|
}
|
||||||
|
|
||||||
public function insert(
|
public function insert(
|
||||||
UserInterface $user,
|
UserInterface $user,
|
||||||
string $url,
|
string $url,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Base\Util\ImdbMatcher;
|
|||||||
use App\Tmdb\Dto\TmdbEpisodeDto;
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
use App\Tmdb\Dto\WatchProviderDto;
|
use App\Tmdb\Dto\WatchProviderDto;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
@@ -60,6 +61,7 @@ class TmdbClient
|
|||||||
private readonly SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
#[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null,
|
#[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null,
|
||||||
) {
|
) {
|
||||||
@@ -293,7 +295,12 @@ class TmdbClient
|
|||||||
!in_array($mediaType, [MediaType::Movie->value, MediaType::TvShow->value])) {
|
!in_array($mediaType, [MediaType::Movie->value, MediaType::TvShow->value])) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return $this->repos[$mediaType]->getApi()->getExternalIds($tmdbId);
|
try {
|
||||||
|
return $this->repos[$mediaType]->getApi()->getExternalIds($tmdbId);
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
$this->logger->warning("[TmdbClient] Error getting external ids for $tmdbId: " . $throwable->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findByImdbId(string $imdbId): array
|
private function findByImdbId(string $imdbId): array
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
|
|
||||||
class UserPreferencesFactory
|
class UserPreferencesFactory
|
||||||
{
|
{
|
||||||
|
public static function createFromArray(array $data): UserPreferences
|
||||||
|
{
|
||||||
|
return new UserPreferences(
|
||||||
|
resolution: static::getArrayValue($data, 'resolution'),
|
||||||
|
codec: static::getArrayValue($data, 'codec'),
|
||||||
|
language: static::getArrayValue($data, 'language'),
|
||||||
|
provider: static::getArrayValue($data, 'provider'),
|
||||||
|
quality: static::getArrayValue($data, 'quality'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param User $user */
|
/** @param User $user */
|
||||||
public static function createFromUser(UserInterface $user): UserPreferences
|
public static function createFromUser(UserInterface $user): UserPreferences
|
||||||
{
|
{
|
||||||
@@ -30,4 +41,21 @@ class UserPreferencesFactory
|
|||||||
$value = explode(',', $value);
|
$value = explode(',', $value);
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function getArrayValue(array $data, string $key): array|null
|
||||||
|
{
|
||||||
|
if (!array_key_exists($key, $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("" === $data[$key]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === is_string($data[$key])) {
|
||||||
|
return [$data[$key]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data[$key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user