diff --git a/assets/components/download-option-tr.js b/assets/components/download-option-tr.js index d6f3787..c50ce95 100644 --- a/assets/components/download-option-tr.js +++ b/assets/components/download-option-tr.js @@ -102,6 +102,14 @@ export default class DownloadOptionTr extends HTMLTableRowElement { } 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', { method: 'POST', headers: { @@ -109,12 +117,11 @@ export default class DownloadOptionTr extends HTMLTableRowElement { 'Accept': 'application/json', }, body: JSON.stringify({ - url: this.url, - title: this.mediaTitle, - filename: this.filename, mediaType: this.mediaType, imdbId: this.imdbId, - episodeId: this.episodeId + season: this.season, + episode: this.episode, + filter: preferences, }) }) .then(res => res.json()) diff --git a/assets/controllers/download_button_controller.js b/assets/controllers/download_button_controller.js index 9f35315..59b7bea 100644 --- a/assets/controllers/download_button_controller.js +++ b/assets/controllers/download_button_controller.js @@ -16,6 +16,7 @@ export default class extends Controller { } download() { + console.log(new FormData(document.querySelector('[name="user_media_preferences_form"]')).values()); fetch('/api/download', { method: 'POST', headers: { @@ -28,7 +29,8 @@ export default class extends Controller { filename: this.filenameValue, mediaType: this.mediaTypeValue, imdbId: this.imdbIdValue, - episodeId: this.episodeIdValue + episodeId: this.episodeIdValue, + filter: new FormData(document.querySelector('[name="user_media_preferences_form"]')).values() }) }) .then(res => res.json()) diff --git a/config/services.yaml b/config/services.yaml index 7bd2fe0..8daf538 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -66,6 +66,7 @@ services: App\: resource: '../src/' exclude: + - '../src/Library/Dto' - '../src/DependencyInjection/' - '../src/Entity/' - '../src/Kernel.php' diff --git a/migrations/Version20260321191300.php b/migrations/Version20260321191300.php new file mode 100644 index 0000000..ef7e870 --- /dev/null +++ b/migrations/Version20260321191300.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/src/Base/Service/MediaFiles.php b/src/Base/Service/MediaFiles.php index cd2ddac..585dbf2 100644 --- a/src/Base/Service/MediaFiles.php +++ b/src/Base/Service/MediaFiles.php @@ -113,13 +113,14 @@ class MediaFiles 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( - '%s' . DIRECTORY_SEPARATOR . '%s (%s)', + '%s' . DIRECTORY_SEPARATOR . '%s (%s) [imdbid-%s]', $this->moviesPath, $title, - $year + $year, + $imdbId ); if (false === $this->filesystem->exists($path)) { @@ -129,13 +130,15 @@ class MediaFiles 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( - '%s' . DIRECTORY_SEPARATOR . '%s (%s)', + '%s' . DIRECTORY_SEPARATOR . '%s (%s) [imdbid-%s]' . DIRECTORY_SEPARATOR . 'Season %s', $this->tvShowsPath, $title, - $year + $year, + $imdbId, + str_pad($season, 2, '0', STR_PAD_LEFT), ); if (false === $this->filesystem->exists($path)) { diff --git a/src/Download/Action/Command/DownloadMediaCommand.php b/src/Download/Action/Command/DownloadMediaCommand.php index 93f6324..034dbf5 100644 --- a/src/Download/Action/Command/DownloadMediaCommand.php +++ b/src/Download/Action/Command/DownloadMediaCommand.php @@ -10,13 +10,14 @@ use OneToMany\RichBundle\Contract\CommandInterface; class DownloadMediaCommand implements CommandInterface { public function __construct( - public string $url, - public string $title, - public string $filename, - public string $mediaType, public string $imdbId, - public int $userId, - public ?int $downloadId = null, + public string $mediaType, + 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, ) {} } \ No newline at end of file diff --git a/src/Download/Action/Handler/DownloadMediaHandler.php b/src/Download/Action/Handler/DownloadMediaHandler.php index 9ffcd30..c493699 100644 --- a/src/Download/Action/Handler/DownloadMediaHandler.php +++ b/src/Download/Action/Handler/DownloadMediaHandler.php @@ -4,13 +4,26 @@ namespace App\Download\Action\Handler; use App\Base\Enum\MediaType; 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\Result\DownloadMediaResult; use App\Download\DownloadEvents; +use App\Download\DownloadOptionEvaluator; use App\Download\Framework\Entity\Download; use App\Download\Framework\Repository\DownloadRepository; use App\Download\Downloader\DownloaderInterface; 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 OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; @@ -18,19 +31,63 @@ use OneToMany\RichBundle\Contract\ResultInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\MessageBusInterface; + /** @implements HandlerInterface */ readonly class DownloadMediaHandler implements HandlerInterface { public function __construct( - private MessageBusInterface $bus, - private DownloaderInterface $downloader, - private DownloadRepository $downloadRepository, - private UserRepository $userRepository, private Broadcaster $broadcaster, + private DownloadOptionEvaluator $downloadOptionEvaluator, + private GetTvShowOptionsHandler $getTvShowOptionsHandler, + private GetMovieOptionsHandler $getMovieOptionsHandler, + 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 { $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( $user, DownloadEvents::DOWNLOAD_STARTED->type(), @@ -38,20 +95,6 @@ readonly class DownloadMediaHandler implements HandlerInterface (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 { $this->validateDownloadUrl($download->getUrl()); } catch (\Throwable $exception) { @@ -69,8 +112,8 @@ readonly class DownloadMediaHandler implements HandlerInterface $this->downloader->download( $command->mediaType, - $command->title, - $command->url, + $download->getUrl(), + $this->getFilepath(MediaType::from($command->mediaType), $media, $matchingOption), $download->getId() ); @@ -91,6 +134,54 @@ readonly class DownloadMediaHandler implements HandlerInterface 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) { $badFileSizes = [ diff --git a/src/Download/Action/Input/DownloadMediaInput.php b/src/Download/Action/Input/DownloadMediaInput.php index 7e00c4c..e2d25ec 100644 --- a/src/Download/Action/Input/DownloadMediaInput.php +++ b/src/Download/Action/Input/DownloadMediaInput.php @@ -3,9 +3,12 @@ namespace App\Download\Action\Input; use App\Download\Action\Command\DownloadMediaCommand; +use OneToMany\RichBundle\Attribute\PropertyIgnored; use OneToMany\RichBundle\Attribute\SourceRequest; +use OneToMany\RichBundle\Attribute\SourceSecurity; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\InputInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** @implements InputInterface */ class DownloadMediaInput implements InputInterface @@ -13,39 +16,60 @@ class DownloadMediaInput implements InputInterface public ?string $mercureAlertTopic = null; public function __construct( - #[SourceRequest('url')] - public string $url, - - #[SourceRequest('title')] - public string $title, - - #[SourceRequest('filename')] - public string $filename, + #[SourceRequest('imdbId')] + public string $imdbId, #[SourceRequest('mediaType')] public string $mediaType, - #[SourceRequest('imdbId')] - public string $imdbId, + #[SourceRequest('season', nullify: true)] + public int|string|null $season = null, - #[SourceRequest('episodeId', nullify: true)] - public ?string $episodeId = null, + #[SourceRequest('episode', nullify: true)] + 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 { return new DownloadMediaCommand( - $this->url, - $this->title, - $this->filename, - $this->mediaType, $this->imdbId, - $this->userId, + $this->mediaType, + $this->season, + $this->episode, + $this->url, + $this->filter, $this->downloadId, + $this->userId, $this->mercureAlertTopic, ); } diff --git a/src/Download/DownloadOptionEvaluator.php b/src/Download/DownloadOptionEvaluator.php index 783acef..7ccfb56 100644 --- a/src/Download/DownloadOptionEvaluator.php +++ b/src/Download/DownloadOptionEvaluator.php @@ -26,13 +26,15 @@ class DownloadOptionEvaluator // return false; //} - if (false === $this->validateDownloadUrl($result->url)) { - return false; - } +// if (false === $this->validateDownloadUrl($result->url)) { +// return false; +// } return true; }); +// dd($filter); + if ($matches->count() > 0) { return Map::from($matches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last(); } diff --git a/src/Download/Downloader/DownloaderInterface.php b/src/Download/Downloader/DownloaderInterface.php index 18b2554..136d430 100644 --- a/src/Download/Downloader/DownloaderInterface.php +++ b/src/Download/Downloader/DownloaderInterface.php @@ -16,5 +16,5 @@ interface DownloaderInterface * @return void * 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; } diff --git a/src/Download/Downloader/ProcessDownloader.php b/src/Download/Downloader/ProcessDownloader.php index a7ea995..b130367 100644 --- a/src/Download/Downloader/ProcessDownloader.php +++ b/src/Download/Downloader/ProcessDownloader.php @@ -24,24 +24,19 @@ class ProcessDownloader implements DownloaderInterface public function __construct( private MessageBusInterface $bus, private EntityManagerInterface $entityManager, - private MediaFiles $mediaFiles, private CacheInterface $cache, private readonly Broadcaster $broadcaster, - private readonly GetMediaInfoHandler $getMediaInfoHandler, ) {} /** * @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 */ $downloadEntity = $this->entityManager->getRepository(Download::class)->find($downloadId); $this->entityManager->flush(); - $downloadPreferences = $downloadEntity->getUser()->getDownloadPreferences(); - $path = $this->getDownloadPath($mediaType, $title, $downloadEntity->getImdbId(), $downloadPreferences); - $processArgs = ['wget', '-O', $downloadEntity->getFilename(), $url]; if ($downloadEntity->getStatus() === 'Paused') { @@ -51,13 +46,9 @@ class ProcessDownloader implements DownloaderInterface $downloadEntity->setProgress(0); } - fwrite(STDOUT, implode(" ", $processArgs)); - - $process = (new Process($processArgs))->setWorkingDirectory($path); - + $process = (new Process($processArgs))->setWorkingDirectory($downloadPath); $process->setTimeout(1800); // 30 min $process->setIdleTimeout(600); // 10 min - $process->start(); try { @@ -96,7 +87,7 @@ class ProcessDownloader implements DownloaderInterface } catch (ProcessFailedException $exception) { $downloadEntity->setStatus('Failed'); $this->bus->dispatch(new AddEventLogCommand( - $downloadEntity->getUser()->getId(), + $downloadEntity->getUser(), DownloadEvents::DOWNLOAD_ERROR->type(), DownloadEvents::DOWNLOAD_ERROR->message() . ': ' . $exception->getMessage(), (array) $downloadEntity @@ -106,27 +97,6 @@ class ProcessDownloader implements DownloaderInterface $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 { if ("tvshows" === $download->getMediaType()) { @@ -137,4 +107,4 @@ class ProcessDownloader implements DownloaderInterface $this->broadcaster->alert('Success', $message, sendPush: true); } -} \ No newline at end of file +} diff --git a/src/Download/Downloader/WgetDownloader.php b/src/Download/Downloader/WgetDownloader.php deleted file mode 100644 index 02524f3..0000000 --- a/src/Download/Downloader/WgetDownloader.php +++ /dev/null @@ -1,27 +0,0 @@ -downloadRepository->insert( + $download = $this->downloadRepository->insertNew( $this->getUser(), - $input->url, - $input->title, - $input->filename, $input->imdbId, $input->mediaType, - $input->episodeId, + $input->season, + $input->episode, ); - $input->downloadId = $download->getId(); - $input->userId = $this->getUser()->getId(); - $input->mercureAlertTopic = $this->requestStack->getSession()->get('mercure_alert_topic'); + $input->setDownloadId($download->getId()); + $input->setUserId($this->getUser()->getId()); + $input->setMercureAlertTopic($this->requestStack->getSession()->get('mercure_alert_topic')); + $input->toCommand(); $this->bus->dispatch(new AddEventLogCommand( $this->getUser(), @@ -60,7 +62,7 @@ class ApiController extends AbstractController $this->broadcaster->alert( title: 'Success', - message: "$input->title added to Queue." + 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']); } + + #[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); + } } diff --git a/src/Download/Framework/Entity/Download.php b/src/Download/Framework/Entity/Download.php index c939e7f..dbb707b 100644 --- a/src/Download/Framework/Entity/Download.php +++ b/src/Download/Framework/Entity/Download.php @@ -30,7 +30,7 @@ class Download #[ORM\Column(length: 255, nullable: true)] private ?string $title = null; - #[ORM\Column(length: 1024)] + #[ORM\Column(length: 1024, nullable: true)] private ?string $url = null; #[ORM\Column(length: 1024, nullable: true)] diff --git a/src/Download/Framework/Repository/DownloadRepository.php b/src/Download/Framework/Repository/DownloadRepository.php index 8461dc3..7b87c85 100644 --- a/src/Download/Framework/Repository/DownloadRepository.php +++ b/src/Download/Framework/Repository/DownloadRepository.php @@ -2,6 +2,7 @@ namespace App\Download\Framework\Repository; +use App\Base\Util\EpisodeId; use App\Base\Util\Paginator; use App\Download\Framework\Entity\Download; use App\User\Framework\Entity\User; @@ -56,6 +57,34 @@ class DownloadRepository extends ServiceEntityRepository 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( UserInterface $user, string $url, diff --git a/src/Tmdb/TmdbClient.php b/src/Tmdb/TmdbClient.php index 69df8da..0e703f1 100644 --- a/src/Tmdb/TmdbClient.php +++ b/src/Tmdb/TmdbClient.php @@ -8,6 +8,7 @@ use App\Base\Util\ImdbMatcher; use App\Tmdb\Dto\TmdbEpisodeDto; use App\Tmdb\Dto\WatchProviderDto; use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\HttpClient; @@ -60,6 +61,7 @@ class TmdbClient private readonly SerializerInterface $serializer, private readonly CacheItemPoolInterface $cache, private readonly EventDispatcherInterface $eventDispatcher, + private readonly LoggerInterface $logger, #[Autowire(env: 'TMDB_API')] string $apiKey, #[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null, ) { @@ -293,7 +295,12 @@ class TmdbClient !in_array($mediaType, [MediaType::Movie->value, MediaType::TvShow->value])) { 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 diff --git a/src/User/Dto/UserPreferencesFactory.php b/src/User/Dto/UserPreferencesFactory.php index 9af72a3..0e3c986 100644 --- a/src/User/Dto/UserPreferencesFactory.php +++ b/src/User/Dto/UserPreferencesFactory.php @@ -8,6 +8,17 @@ use Symfony\Component\Security\Core\User\UserInterface; 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 */ public static function createFromUser(UserInterface $user): UserPreferences { @@ -30,4 +41,21 @@ class UserPreferencesFactory $value = explode(',', $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]; + } }