Compare commits

..

3 Commits

Author SHA1 Message Date
Brock H Caldwell
e39cb6e9bd WIP: working move/tv show download 2026-03-21 23:22:41 -05:00
Brock H Caldwell
9e2c5410ba Merge branch 'dev-filename-convention'
All checks were successful
CI / build-test (push) Successful in 2m57s
2026-03-18 09:29:17 -05:00
Brock H Caldwell
d358ef8de6 fix: adds build on push to main
All checks were successful
CI / build-test (push) Successful in 2m55s
2026-03-17 23:19:23 -05:00
18 changed files with 327 additions and 132 deletions

View File

@@ -2,6 +2,8 @@ name: CI
on: on:
push: push:
branches:
- "main"
tags: tags:
- "v*" - "v*"

View File

@@ -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())

View File

@@ -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())

View File

@@ -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'

View 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);
}
}

View File

@@ -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)) {

View File

@@ -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,
) {} ) {}
} }

View File

@@ -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 = [

View File

@@ -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,
); );
} }

View File

@@ -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();
} }

View File

@@ -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;
} }

View File

@@ -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()) {

View File

@@ -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
));
}
}

View File

@@ -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);
}
} }

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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

View File

@@ -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];
}
} }