From dc9242d96e15b0967818bc267b5b17757739c25c Mon Sep 17 00:00:00 2001 From: Brock H Caldwell Date: Tue, 22 Jul 2025 22:49:07 -0500 Subject: [PATCH] fix: links to episodes from downloads --- assets/bootstrap.js | 1 + .../controllers/tv_episode_list_controller.js | 6 +++ config/packages/pwa.yaml | 4 +- .../Action/Command/GetMediaInfoCommand.php | 1 + .../Action/Handler/GetMediaInfoHandler.php | 2 +- src/Search/Action/Input/GetMediaInfoInput.php | 9 ++++- .../Action/Result/GetMediaInfoResult.php | 1 + .../Framework/Controller/WebController.php | 2 +- src/Twig/Components/TvEpisodeList.php | 16 ++++++++ src/Twig/Dto/EpisodeIdDto.php | 25 ++++++++++++ src/Twig/Extensions/UtilExtension.php | 39 +++++++++++++++++++ .../components/DownloadListRow.html.twig | 13 +++++-- templates/components/NavBar.html.twig | 2 +- templates/components/TvEpisodeList.html.twig | 4 +- templates/search/result.html.twig | 2 +- 15 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 src/Twig/Dto/EpisodeIdDto.php 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 @@ - + {% if download.mediaType == "movies" %} + {% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType} %} + {% set route = path('app_search_result', routeParams) %} + {% else %} + {% set episodeIdDto = extract_from_episode_id(download.episodeId) %} + {% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType, season: episodeIdDto.season, episode: episodeIdDto.episode} %} + {% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %} + {% endif %} + {{ download.title }} diff --git a/templates/components/NavBar.html.twig b/templates/components/NavBar.html.twig index 08d92bd..6de8e87 100644 --- a/templates/components/NavBar.html.twig +++ b/templates/components/NavBar.html.twig @@ -1,4 +1,4 @@ -