fix: missing/exists badge on tvshows results
This commit is contained in:
@@ -6,6 +6,14 @@ controllersBase:
|
|||||||
defaults:
|
defaults:
|
||||||
schemes: [ 'https' ]
|
schemes: [ 'https' ]
|
||||||
|
|
||||||
|
controllersLibrary:
|
||||||
|
resource:
|
||||||
|
path: ../src/Library/Framework/Controller/
|
||||||
|
namespace: App\Library\Framework\Controller
|
||||||
|
type: attribute
|
||||||
|
defaults:
|
||||||
|
schemes: [ 'https' ]
|
||||||
|
|
||||||
controllersSearch:
|
controllersSearch:
|
||||||
resource:
|
resource:
|
||||||
path: ../src/Search/Framework/Controller/
|
path: ../src/Search/Framework/Controller/
|
||||||
|
|||||||
16
src/Library/Action/Command/SearchCommand.php
Normal file
16
src/Library/Action/Command/SearchCommand.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
class SearchCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?string $term = null,
|
||||||
|
public ?string $title = null,
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
public ?string $season = null,
|
||||||
|
public ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
63
src/Library/Action/Handler/SearchHandler.php
Normal file
63
src/Library/Action/Handler/SearchHandler.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
|
use App\Library\Action\Command\SearchCommand;
|
||||||
|
use App\Library\Action\Result\SearchResult;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements HandlerInterface<SearchCommand,SearchHandler>
|
||||||
|
*/
|
||||||
|
class SearchHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
private array $searchTypes = [
|
||||||
|
'episode_by_title' => 'episodeByTitle',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly MediaFiles $mediaFiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
|
{
|
||||||
|
$searchType = $this->getSearchType($command);
|
||||||
|
$function = $this->searchTypes[$searchType];
|
||||||
|
return $this->$function($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSearchType(CommandInterface $command): ?string
|
||||||
|
{
|
||||||
|
if ((!is_null($command->title) || is_null($command->imdbId)) &&
|
||||||
|
!is_null($command->season) &&
|
||||||
|
!is_null($command->episode)
|
||||||
|
) {
|
||||||
|
return 'episode_by_title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function episodeByTitle(CommandInterface $command): ?SearchResult
|
||||||
|
{
|
||||||
|
$result = $this->mediaFiles->episodeExists(
|
||||||
|
$command->title,
|
||||||
|
(int) $command->season,
|
||||||
|
(int) $command->episode,
|
||||||
|
);
|
||||||
|
|
||||||
|
$exists = $result instanceof \SplFileInfo;
|
||||||
|
|
||||||
|
return new SearchResult(
|
||||||
|
input: $command,
|
||||||
|
message: 'Success',
|
||||||
|
code: 200,
|
||||||
|
data: [
|
||||||
|
'exists' => $exists,
|
||||||
|
'file' => true === $exists ? ['filename' => $result->getFilename(), 'size' => $result->getSize()] : null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Library/Action/Input/SearchInput.php
Normal file
38
src/Library/Action/Input/SearchInput.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Input;
|
||||||
|
|
||||||
|
use App\Library\Action\Command\SearchCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceQuery;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements InputInterface<SearchInput, SearchCommand>
|
||||||
|
*/
|
||||||
|
class SearchInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceQuery('term', nullify: true)]
|
||||||
|
private ?string $term = null,
|
||||||
|
#[SourceQuery('title', nullify: true)]
|
||||||
|
private ?string $title = null,
|
||||||
|
#[SourceQuery('imdbId', nullify: true)]
|
||||||
|
private ?string $imdbId = null,
|
||||||
|
#[SourceQuery('season', nullify: true)]
|
||||||
|
private ?string $season = null,
|
||||||
|
#[SourceQuery('episode', nullify: true)]
|
||||||
|
private ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): CommandInterface
|
||||||
|
{
|
||||||
|
return new SearchCommand(
|
||||||
|
term: $this->term,
|
||||||
|
title: $this->title,
|
||||||
|
imdbId: $this->imdbId,
|
||||||
|
season: $this->season,
|
||||||
|
episode: $this->episode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Library/Action/Result/SearchResult.php
Normal file
15
src/Library/Action/Result/SearchResult.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class SearchResult implements ResultInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public object|array $input,
|
||||||
|
public string $message,
|
||||||
|
public int $code,
|
||||||
|
public ?array $data,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
40
src/Library/Framework/Controller/Api.php
Normal file
40
src/Library/Framework/Controller/Api.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Library\Action\Handler\SearchHandler;
|
||||||
|
use App\Library\Action\Input\SearchInput;
|
||||||
|
use App\Library\Action\Result\SearchResult;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\UX\Turbo\TurboBundle;
|
||||||
|
|
||||||
|
class Api extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/api/library/search', name: 'api.library.search', methods: ['GET'])]
|
||||||
|
public function search(SearchInput $input, SearchHandler $handler, Request $request): Response
|
||||||
|
{
|
||||||
|
$result = $handler->handle($input->toCommand());
|
||||||
|
|
||||||
|
if ($request->headers->get('Turbo-Frame')) {
|
||||||
|
return $this->sendFragmentResponse($result, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($handler->handle($input->toCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendFragmentResponse(SearchResult $result, Request $request): Response
|
||||||
|
{
|
||||||
|
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||||
|
return $this->renderBlock(
|
||||||
|
'search/fragments.html.twig',
|
||||||
|
$request->query->get('block'),
|
||||||
|
[
|
||||||
|
'result' => $result,
|
||||||
|
'target' => $request->query->get('target')
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,19 +15,12 @@ class GetMediaInfoHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly Tmdb $tmdb,
|
||||||
private readonly MediaFiles $mediaFiles
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||||
|
|
||||||
if ("tvshows" === $command->mediaType) {
|
|
||||||
foreach ($media->episodes[$command->season] as $key => $episode) {
|
|
||||||
$media->episodes[$command->season][$key]['file'] = $this->mediaFiles->episodeExists($media->title, $command->season, $episode['episode_number']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GetMediaInfoResult($media, $command->season);
|
return new GetMediaInfoResult($media, $command->season);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class UtilExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[AsTwigFilter('episode_id_from_results')]
|
#[AsTwigFilter('episode_id_from_results')]
|
||||||
public function episodeId($result): ?string
|
public function episodeIdFromResults($result): ?string
|
||||||
{
|
{
|
||||||
if (!$result instanceof GetTvShowOptionsResult) {
|
if (!$result instanceof GetTvShowOptionsResult) {
|
||||||
return null;
|
return null;
|
||||||
@@ -56,4 +56,11 @@ class UtilExtension
|
|||||||
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
|
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
|
||||||
"E". str_pad($result->episode, 2, "0", STR_PAD_LEFT);
|
"E". str_pad($result->episode, 2, "0", STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[AsTwigFunction('episode_id')]
|
||||||
|
public function episodeId($season, $episode): ?string
|
||||||
|
{
|
||||||
|
return "S". str_pad($season, 2, "0", STR_PAD_LEFT) .
|
||||||
|
"E". str_pad($episode, 2, "0", STR_PAD_LEFT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,30 +40,17 @@
|
|||||||
{{ episode['air_date']|date(null, 'UTC') }}
|
{{ episode['air_date']|date(null, 'UTC') }}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
{% if episode['file'] != false %}
|
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
||||||
<span data-controller="popover">
|
title: this.title,
|
||||||
<template data-popover-target="content">
|
season: episode['season_number'],
|
||||||
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
episode: episode['episode_number'],
|
||||||
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
block: 'media_exists_badge',
|
||||||
<ul class="list-disc ml-3">
|
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
||||||
<li class="font-normal">{{ episode['file'].realPath|strip_media_path }} — <strong>{{ episode['file'].size|filesize }}</strong></li>
|
}) }}">
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<small
|
|
||||||
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
|
||||||
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
|
||||||
>
|
|
||||||
exists
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if episode['file'] == false %}
|
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||||
missing
|
missing
|
||||||
</small>
|
</small>
|
||||||
{% endif %}
|
</twig:Turbo:Frame>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 justify-between">
|
<div class="flex flex-col gap-4 justify-between">
|
||||||
|
|||||||
32
templates/search/fragments.html.twig
Normal file
32
templates/search/fragments.html.twig
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% block media_exists_badge %}
|
||||||
|
<turbo-stream action="replace" targets="#{{ target }}">
|
||||||
|
<template>
|
||||||
|
{% if result.data['exists'] == true %}
|
||||||
|
<span data-controller="popover">
|
||||||
|
<template data-popover-target="content">
|
||||||
|
<div data-popover-target="card"
|
||||||
|
class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||||
|
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||||
|
<ul class="list-disc ml-3">
|
||||||
|
<li class="font-normal">{{ result.data['file']['filename']|strip_media_path }} — <strong>{{ result.data['file']['size']|filesize }}</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<small
|
||||||
|
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||||
|
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||||
|
>
|
||||||
|
exists
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if result.data['exists'] == false %}
|
||||||
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white"
|
||||||
|
title="Episode has not been downloaded yet.">
|
||||||
|
missing
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</template>
|
||||||
|
</turbo-stream>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user