diff --git a/assets/bootstrap.js b/assets/bootstrap.js index 9b07d31..4bd24c8 100644 --- a/assets/bootstrap.js +++ b/assets/bootstrap.js @@ -2,6 +2,7 @@ import { startStimulusApp } from '@symfony/stimulus-bundle'; import Popover from '@stimulus-components/popover' import Dialog from '@stimulus-components/dialog' import Dropdown from '@stimulus-components/dropdown' +import 'animate.css' const app = startStimulusApp(); // register any custom, 3rd party controllers here diff --git a/assets/controllers/tv_episode_list_controller.js b/assets/controllers/tv_episode_list_controller.js index f767614..e418a2f 100644 --- a/assets/controllers/tv_episode_list_controller.js +++ b/assets/controllers/tv_episode_list_controller.js @@ -14,6 +14,11 @@ export default class extends Controller { this.component.on('render:finished', (component) => { console.log(component); }); + if (window.location.hash) { + let targetElement = document.querySelector(window.location.hash); + targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + targetElement.classList.add('animate__animated', 'animate__pulse', 'animate__faster'); + } } setSeason(season) { @@ -25,6 +30,7 @@ export default class extends Controller { paginate(event) { this.element.querySelectorAll(".episode-container").forEach(element => element.remove()); + this.component.set('episodeNumber', null); this.component.action('paginate', {page: event.params.page}); this.component.render(); } diff --git a/config/packages/pwa.yaml b/config/packages/pwa.yaml index 61e48f4..20ee0d7 100644 --- a/config/packages/pwa.yaml +++ b/config/packages/pwa.yaml @@ -10,9 +10,9 @@ pwa: theme_color: "#083344" description: Torsearch provides a simple and intuitive way to manage your personal media library. icons: - - src: "icon.png" + - src: "/icon.png" sizes: [ 192 ] - - src: "icon.png" + - src: "/icon.png" sizes: [ 192 ] purpose: maskable categories: diff --git a/src/Search/Action/Command/GetMediaInfoCommand.php b/src/Search/Action/Command/GetMediaInfoCommand.php index 403e021..ffd1609 100644 --- a/src/Search/Action/Command/GetMediaInfoCommand.php +++ b/src/Search/Action/Command/GetMediaInfoCommand.php @@ -11,5 +11,6 @@ class GetMediaInfoCommand implements CommandInterface public string $imdbId, public string $mediaType, public ?int $season = null, + public ?int $episode = null, ) {} } \ No newline at end of file diff --git a/src/Search/Action/Handler/GetMediaInfoHandler.php b/src/Search/Action/Handler/GetMediaInfoHandler.php index 24ae107..14a9e40 100644 --- a/src/Search/Action/Handler/GetMediaInfoHandler.php +++ b/src/Search/Action/Handler/GetMediaInfoHandler.php @@ -20,6 +20,6 @@ class GetMediaInfoHandler implements HandlerInterface { $media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType); - return new GetMediaInfoResult($media, $command->season); + return new GetMediaInfoResult($media, $command->season, $command->episode); } } diff --git a/src/Search/Action/Input/GetMediaInfoInput.php b/src/Search/Action/Input/GetMediaInfoInput.php index dc0e607..b91a464 100644 --- a/src/Search/Action/Input/GetMediaInfoInput.php +++ b/src/Search/Action/Input/GetMediaInfoInput.php @@ -19,6 +19,9 @@ class GetMediaInfoInput implements InputInterface #[SourceRoute('season', nullify: true)] public ?int $season, + + #[SourceRoute('episode', nullify: true)] + public ?int $episode, ) {} public function toCommand(): CommandInterface @@ -26,6 +29,10 @@ class GetMediaInfoInput implements InputInterface if ("tvshows" === $this->mediaType && null === $this->season) { $this->season = 1; } - return new GetMediaInfoCommand($this->imdbId, $this->mediaType, $this->season); + + if ("tvshows" === $this->mediaType && null === $this->episode) { + $this->episode = 1; + } + return new GetMediaInfoCommand($this->imdbId, $this->mediaType, $this->season, $this->episode); } } \ No newline at end of file diff --git a/src/Search/Action/Result/GetMediaInfoResult.php b/src/Search/Action/Result/GetMediaInfoResult.php index 7d45c92..02f505d 100644 --- a/src/Search/Action/Result/GetMediaInfoResult.php +++ b/src/Search/Action/Result/GetMediaInfoResult.php @@ -11,5 +11,6 @@ class GetMediaInfoResult implements ResultInterface public function __construct( public TmdbResult $media, public ?int $season, + public ?int $episode, ) {} } diff --git a/src/Search/Framework/Controller/WebController.php b/src/Search/Framework/Controller/WebController.php index 5daada6..7348c12 100644 --- a/src/Search/Framework/Controller/WebController.php +++ b/src/Search/Framework/Controller/WebController.php @@ -33,7 +33,7 @@ final class WebController extends AbstractController ]); } - #[Route('/result/{mediaType}/{imdbId}/{season}', name: 'app_search_result')] + #[Route('/result/{mediaType}/{imdbId}/{season}/{episode?}', name: 'app_search_result')] public function result( GetMediaInfoInput $input, ?int $season = null, diff --git a/src/Twig/Components/TvEpisodeList.php b/src/Twig/Components/TvEpisodeList.php index 33ff44f..719fa59 100644 --- a/src/Twig/Components/TvEpisodeList.php +++ b/src/Twig/Components/TvEpisodeList.php @@ -6,6 +6,8 @@ use App\Search\Action\Command\GetMediaInfoCommand; use App\Search\Action\Handler\GetMediaInfoHandler; use App\Search\TvEpisodePaginator; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveAction; +use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; @@ -27,6 +29,12 @@ final class TvEpisodeList #[LiveProp(writable: true)] public int $season = 1; + #[LiveProp(writable: true)] + public int $reloadCount = 0; + + #[LiveProp(writable: true)] + public ?int $episodeNumber = null; + public function __construct( private GetMediaInfoHandler $getMediaInfoHandler, ) {} @@ -34,6 +42,14 @@ final class TvEpisodeList public function getEpisodes() { $results = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand($this->imdbId, "tvshows", $this->season)); + + if (null !== $this->episodeNumber) { + $this->pageNumber = ceil($this->episodeNumber / $this->perPage); + $this->episodeNumber = null; + } + + $this->reloadCount++; + return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage); } diff --git a/src/Twig/Dto/EpisodeIdDto.php b/src/Twig/Dto/EpisodeIdDto.php new file mode 100644 index 0000000..19418f7 --- /dev/null +++ b/src/Twig/Dto/EpisodeIdDto.php @@ -0,0 +1,25 @@ +season, 2, "0", STR_PAD_LEFT) . + "E". str_pad($this->episode, 2, "0", STR_PAD_LEFT); + } + + public function __toString(): string + { + if ("" !== $this->season && "" !== $this->episode) { + return $this->asEpisodeId(); + } + return ""; + } +} diff --git a/src/Twig/Extensions/UtilExtension.php b/src/Twig/Extensions/UtilExtension.php index a591490..f005089 100644 --- a/src/Twig/Extensions/UtilExtension.php +++ b/src/Twig/Extensions/UtilExtension.php @@ -4,6 +4,7 @@ namespace App\Twig\Extensions; use App\Base\Service\MediaFiles; use App\Torrentio\Action\Result\GetTvShowOptionsResult; +use App\Twig\Dto\EpisodeIdDto; use ChrisUllyott\FileSize; use Twig\Attribute\AsTwigFilter; use Twig\Attribute\AsTwigFunction; @@ -63,4 +64,42 @@ class UtilExtension return "S". str_pad($season, 2, "0", STR_PAD_LEFT) . "E". str_pad($episode, 2, "0", STR_PAD_LEFT); } + + #[AsTwigFunction('episode_anchor')] + public function episodeAnchor($season, $episode): ?string + { + return "episode_" . $season . "_" . $episode; + } + + #[AsTwigFunction('extract_from_episode_id')] + public function extractFromEpisodeId(?string $episodeId): ?EpisodeIdDto + { + if (null === $episodeId) { + return new EpisodeIdDto("", ""); + } + + // Capture season + $seasonMatch = []; + preg_match('/[sS]\d\d/', $episodeId, $seasonMatch); + if (empty($seasonMatch)) { + $season = ""; + } else { + $season = str_replace(['S', 's'], '', $seasonMatch[0]); + } + + // Capture episode + $episodeMatch = []; + preg_match('/[eE]\d\d/', $episodeId, $episodeMatch); + if (empty($episodeMatch)) { + $episode = ""; + } else { + $episode = str_replace(['E', 'e'], '', $episodeMatch[0]); + } + + if (null === $season && null === $episode) { + return new EpisodeIdDto("", ""); + } + + return new EpisodeIdDto($season, $episode); + } } diff --git a/templates/components/DownloadListRow.html.twig b/templates/components/DownloadListRow.html.twig index 3cea89e..2f055ba 100644 --- a/templates/components/DownloadListRow.html.twig +++ b/templates/components/DownloadListRow.html.twig @@ -1,8 +1,15 @@