feat: tv episode results
This commit is contained in:
26
assets/controllers/tv_results_controller.js
Normal file
26
assets/controllers/tv_results_controller.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
tmdbId: String,
|
||||
imdbId: String,
|
||||
season: String,
|
||||
episode: String,
|
||||
active: Boolean,
|
||||
};
|
||||
|
||||
connect() {
|
||||
if (true === this.activeValue) {
|
||||
fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||
.then(res => res.text())
|
||||
.then(response => {
|
||||
this.element.innerHTML = response;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -12,9 +14,10 @@ final class TorrentioController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||
) {}
|
||||
|
||||
#[Route('/torrentio/movies/{imdbId}', name: 'app_torrentio')]
|
||||
#[Route('/torrentio/movies/{imdbId}', name: 'app_torrentio_movies')]
|
||||
public function movieOptions(GetMovieOptionsInput $input): Response
|
||||
{
|
||||
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||
@@ -23,4 +26,14 @@ final class TorrentioController extends AbstractController
|
||||
'results' => $results,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
|
||||
public function tvShowOptions(GetTvShowOptionsInput $input): Response
|
||||
{
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ use Tmdb\Model\Search\SearchQuery\MovieSearchQuery;
|
||||
use Tmdb\Model\Tv;
|
||||
use Tmdb\Repository\MovieRepository;
|
||||
use Tmdb\Repository\SearchRepository;
|
||||
use Tmdb\Repository\TvEpisodeRepository;
|
||||
use Tmdb\Repository\TvRepository;
|
||||
use Tmdb\Repository\TvSeasonRepository;
|
||||
use Tmdb\Token\Api\ApiToken;
|
||||
@@ -139,7 +140,14 @@ class Tmdb
|
||||
$client = new TvRepository($this->client);
|
||||
$details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
|
||||
$details = $this->getEpisodesFromSeries($details);
|
||||
return $this->parseResult($details, "tv");
|
||||
return $this->parseResult($details, "tvshow");
|
||||
}
|
||||
|
||||
public function episodeDetails(string $id, string $season, string $episode)
|
||||
{
|
||||
$client = new TvEpisodeRepository($this->client);
|
||||
$result = $client->getApi()->getEpisode($id, $season, $episode, ['append_to_response' => 'external_ids']);
|
||||
return $this->parseResult($result, "episode");
|
||||
}
|
||||
|
||||
public function getEpisodesFromSeries(array $series)
|
||||
@@ -192,6 +200,19 @@ class Tmdb
|
||||
);
|
||||
}
|
||||
|
||||
function parseEpisode(array $data, string $posterBasePath): TmdbResult {
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: $posterBasePath . $data['still_path'],
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: null,
|
||||
);
|
||||
}
|
||||
|
||||
function parseMovie(array $data, string $posterBasePath): TmdbResult {
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
@@ -204,8 +225,13 @@ class Tmdb
|
||||
);
|
||||
}
|
||||
|
||||
$posterBasePath = self::POSTER_IMG_PATH;
|
||||
$result = ("movie" === $mediaType) ? parseMovie($data, $posterBasePath) : parseTvShow($data, $posterBasePath);
|
||||
if ($mediaType === 'movie') {
|
||||
$result = parseMovie($data, self::POSTER_IMG_PATH);
|
||||
} elseif ($mediaType === 'tvshow') {
|
||||
$result = parseTvShow($data, self::POSTER_IMG_PATH);
|
||||
} elseif ($mediaType === 'episode') {
|
||||
$result = parseEpisode($data, self::POSTER_IMG_PATH);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
15
src/Torrentio/Action/Command/GetTvShowOptionsCommand.php
Normal file
15
src/Torrentio/Action/Command/GetTvShowOptionsCommand.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class GetTvShowOptionsCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $tmdbId,
|
||||
public string $imdbId,
|
||||
public ?string $season,
|
||||
public ?string $episode,
|
||||
) {}
|
||||
}
|
||||
32
src/Torrentio/Action/Handler/GetTvShowOptionsHandler.php
Normal file
32
src/Torrentio/Action/Handler/GetTvShowOptionsHandler.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Action\Handler;
|
||||
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||
use App\Torrentio\Client\Torrentio;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class GetTvShowOptionsHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly Torrentio $torrentio,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
return new GetTvShowOptionsResult(
|
||||
media: $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode),
|
||||
season: $command->season,
|
||||
episode: $command->episode,
|
||||
results: $this->torrentio->fetchEpisodeResults(
|
||||
$command->imdbId,
|
||||
$command->season,
|
||||
$command->episode,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/Torrentio/Action/Input/GetTvShowOptionsInput.php
Normal file
32
src/Torrentio/Action/Input/GetTvShowOptionsInput.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Action\Input;
|
||||
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
class GetTvShowOptionsInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceRoute('tmdbId')]
|
||||
public string $tmdbId,
|
||||
#[SourceRoute('imdbId')]
|
||||
public string $imdbId,
|
||||
#[SourceRoute('season', nullify: true)]
|
||||
public ?string $season,
|
||||
#[SourceRoute('episode', nullify: true)]
|
||||
public string $episode,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new GetTvShowOptionsCommand(
|
||||
$this->tmdbId,
|
||||
$this->imdbId,
|
||||
$this->season,
|
||||
$this->episode
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Torrentio/Action/Result/GetTvShowOptionsResult.php
Normal file
16
src/Torrentio/Action/Result/GetTvShowOptionsResult.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Action\Result;
|
||||
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class GetTvShowOptionsResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public string $season,
|
||||
public string $episode,
|
||||
public array $results
|
||||
) {}
|
||||
}
|
||||
@@ -19,8 +19,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if "movies" == results.media.mediaType %}
|
||||
<div id="results" {{ stimulus_controller('movie_results', {imdbId: results.media.imdbId}) }}>
|
||||
</div>
|
||||
{% elseif "tvshows" == results.media.mediaType %}
|
||||
{% for season, episodes in results.media.episodes %}
|
||||
{% set active = (season == '1') ? true : false %}
|
||||
{% for episode in episodes %}
|
||||
<div id="results" {{ stimulus_controller('tv_results', {
|
||||
tmdbId: results.media.tmdbId,
|
||||
imdbId: results.media.imdbId,
|
||||
season: season,
|
||||
episode: episode['episode_number'],
|
||||
active: active,
|
||||
}) }}>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
<div class="p-2 flex flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
<h3 class="text-md">Episodes</h3>
|
||||
</div>
|
||||
<div class="p-2 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
<h4 class="text-md font-bold">Episode {{ results.episode }}</h4>
|
||||
<div class="flex flex-row gap-4">
|
||||
<img class="w-24 rounded-lg" src="{{ results.media.poster }}" />
|
||||
<div class="flex flex-col gap-4">
|
||||
<h5 class="text-md font-bold">{{ results.media.title }}</h5>
|
||||
<p>{{ results.media.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-transparent rounded-lg">
|
||||
<thead>
|
||||
<tr class="dark:bg-stone-600 overflow-hidden rounded-md">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-gray-50">
|
||||
Size
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
Resolution
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
Codec
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
Seeders
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
Provider
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
Language
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-gray-50">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-50">
|
||||
{% for result in results.results %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.size }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.resolution }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.codec }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.seeders }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.provider }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||
{{ result.languageFlags }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<span class="p-1.5 bg-green-600 rounded-md">
|
||||
<span class="text-gray-50">Download</span>
|
||||
</span>
|
||||
<input class="ml-1" type="checkbox" name="select" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user