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;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||||
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||||
|
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@@ -12,9 +14,10 @@ final class TorrentioController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
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
|
public function movieOptions(GetMovieOptionsInput $input): Response
|
||||||
{
|
{
|
||||||
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||||
@@ -23,4 +26,14 @@ final class TorrentioController extends AbstractController
|
|||||||
'results' => $results,
|
'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\Model\Tv;
|
||||||
use Tmdb\Repository\MovieRepository;
|
use Tmdb\Repository\MovieRepository;
|
||||||
use Tmdb\Repository\SearchRepository;
|
use Tmdb\Repository\SearchRepository;
|
||||||
|
use Tmdb\Repository\TvEpisodeRepository;
|
||||||
use Tmdb\Repository\TvRepository;
|
use Tmdb\Repository\TvRepository;
|
||||||
use Tmdb\Repository\TvSeasonRepository;
|
use Tmdb\Repository\TvSeasonRepository;
|
||||||
use Tmdb\Token\Api\ApiToken;
|
use Tmdb\Token\Api\ApiToken;
|
||||||
@@ -139,7 +140,14 @@ class Tmdb
|
|||||||
$client = new TvRepository($this->client);
|
$client = new TvRepository($this->client);
|
||||||
$details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
|
$details = $client->getApi()->getTvshow($id, ['append_to_response' => 'external_ids,seasons']);
|
||||||
$details = $this->getEpisodesFromSeries($details);
|
$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)
|
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 {
|
function parseMovie(array $data, string $posterBasePath): TmdbResult {
|
||||||
return new TmdbResult(
|
return new TmdbResult(
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
imdbId: $data['external_ids']['imdb_id'],
|
||||||
@@ -204,8 +225,13 @@ class Tmdb
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$posterBasePath = self::POSTER_IMG_PATH;
|
if ($mediaType === 'movie') {
|
||||||
$result = ("movie" === $mediaType) ? parseMovie($data, $posterBasePath) : parseTvShow($data, $posterBasePath);
|
$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;
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if "movies" == results.media.mediaType %}
|
||||||
<div id="results" {{ stimulus_controller('movie_results', {imdbId: results.media.imdbId}) }}>
|
<div id="results" {{ stimulus_controller('movie_results', {imdbId: results.media.imdbId}) }}>
|
||||||
</div>
|
</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>
|
</twig:Card>
|
||||||
</div>
|
</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">
|
<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">
|
||||||
<h3 class="text-md">Episodes</h3>
|
<h4 class="text-md font-bold">Episode {{ results.episode }}</h4>
|
||||||
</div>
|
<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