Compare commits

...

15 Commits

Author SHA1 Message Date
7562597629 fix(monitor): adds null checkk and handles accordingly 2025-09-18 21:02:47 -05:00
deb0333635 fix: tmdb episode details failing from missing imdb id 2025-09-18 18:58:43 -05:00
c8e190f9e8 fix: removes deep link to episode for monitors since episode doesnt always exist and parsing can be iffy 2025-09-17 13:57:45 -05:00
538fde40fe fix: actually fixes broken monitor -> media links 2025-09-17 13:10:07 -05:00
da7a267e2a fix(GetMediaInfoHandler): missing handler for TvEpisode type 2025-09-17 11:52:41 -05:00
daf9b2c18b fix: styles tweaks 2025-09-17 11:47:31 -05:00
d9e5e62f5d chore: style tweak 2025-09-17 11:40:37 -05:00
d4fc7693e3 fix: updates current season when switching seasons 2025-09-17 11:06:04 -05:00
1263ad20a6 fix: reverts back to using dedicated dev dockerfile 2025-09-17 10:15:55 -05:00
e8764bb13b fix: reverts back to using dedicated dev dockerfile 2025-09-17 10:08:22 -05:00
a267bab86e chore: cleanup 2025-09-16 22:13:39 -05:00
9653189bff feat: lazy loads torrentio results on episodes, adds loading indicator for episodes and movies 2025-09-16 21:48:23 -05:00
36836c4d36 fix: search results not rendering automatically 2025-09-16 17:03:40 -05:00
61e4b25212 feat: makes events clickable 2025-09-16 13:17:29 -05:00
209266597e dev: uses base images in local dev 2025-09-16 12:19:40 -05:00
16 changed files with 81 additions and 74 deletions

View File

@@ -1,19 +1,10 @@
FROM dunglas/frankenphp:php8.4 FROM code.caldwell.digital/home/torsearch-base:php8.4
ENV SERVER_NAME=":80" ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off" ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime" ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ENV APP_VERSION="0.0.1" ENV APP_VERSION="0.0.0-dev"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
RUN apt update && apt install -y wget
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ] HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile

View File

@@ -13,7 +13,6 @@ export default class extends Controller {
}; };
static targets = ['list'] static targets = ['list']
static outlets = ['loading-icon']
options = [] options = []
optionsLoaded = false optionsLoaded = false
@@ -28,7 +27,6 @@ export default class extends Controller {
this.options = this.element.querySelectorAll('tbody tr'); this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue); this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.resultCountEl.innerText = this.options.length; this.resultCountEl.innerText = this.options.length;
this.loadingIconOutlet.toggleIcon();
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}})); document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
} }
} }

View File

@@ -22,7 +22,7 @@ export default class extends Controller {
defaultOptions = '<option value="-">-</option>'; defaultOptions = '<option value="-">-</option>';
static outlets = ['tv-episode-list'] static outlets = ['tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected'] static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected', 'currentSeason']
static values = { static values = {
'imdbId': String, 'imdbId': String,
'media-type': String, 'media-type': String,
@@ -32,7 +32,6 @@ export default class extends Controller {
async connect() { async connect() {
await this.setInitialFilter(); await this.setInitialFilter();
this.setTimerToStopLoadingIcon();
this.element.filterResults = this.filter.bind(this); this.element.filterResults = this.filter.bind(this);
document.addEventListener('optionsLoaded', this.loadOptions.bind(this)); document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
} }
@@ -48,10 +47,6 @@ export default class extends Controller {
} }
} }
setTimerToStopLoadingIcon() {
setTimeout(() => this.loadingIconTarget.hideIcon(), 10000);
}
// Event is fired from movies/tvshows controllers to populate this data // Event is fired from movies/tvshows controllers to populate this data
async loadOptions({detail: { options }}) { async loadOptions({detail: { options }}) {
await options.forEach((option) => { await options.forEach((option) => {
@@ -99,7 +94,9 @@ export default class extends Controller {
} }
setSeason(event) { setSeason(event) {
console.log('hurrrr');
this.tvEpisodeListOutlet.setSeason(event.target.value); this.tvEpisodeListOutlet.setSeason(event.target.value);
this.currentSeasonTarget.innerText = event.target.value;
} }
downloadSeason() { downloadSeason() {

View File

@@ -38,21 +38,21 @@ export default class extends Controller {
return ` return `
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon"> <span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg> <svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
</span> </span>`;
`;
} }
event.detail.options.render.option = (data, escape) => { event.detail.options.render.option = (data, escape) => {
if (data.data.description.length > 60) { console.log(data);
data.data.description = data.data.description.substring(0, 107) + "..."; if (data.data.overview.length > 60) {
data.data.overview = data.data.overview.substring(0, 107) + "...";
} }
return `<div class="flex flex-row"> return `<div class="flex flex-row">
<img src="${data.data.poster}" class="w-16 rounded-md"> <img src="${data.data.poster}" class="w-16 rounded-md">
<div class="p-2 flex flex-col"> <div class="p-2 flex flex-col">
<h2>${data.data.title}</h2> <h2>${data.data.title}</h2>
<p class="max-w-[60ch] text-wrap">${data.data.description}</p> <p class="max-w-[60ch] text-wrap">${data.data.overview}</p>
</div> </div>
</div> </div>`;
`
} }
} }

View File

@@ -19,7 +19,6 @@ export default class extends Controller {
}; };
static targets = ['list', 'count', 'episodeSelector',] static targets = ['list', 'count', 'episodeSelector',]
static outlets = ['loading-icon']
options = [] options = []
@@ -35,6 +34,5 @@ export default class extends Controller {
this.countTarget.innerText = 0; this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true; this.episodeSelectorTarget.disabled = true;
} }
this.loadingIconOutlet.increaseCount();
} }
} }

View File

@@ -199,6 +199,10 @@ dialog[data-dialog-target="dialog"][closing] {
@apply flex flex-col gap-1 justify-between; @apply flex flex-col gap-1 justify-between;
} }
/** FullCalendar **/
#upcoming_episodes_calendar .fc-event-main .fc-event-title-container {
cursor: pointer !important;
}
.fc-col-header-cell { .fc-col-header-cell {
@apply bg-orange-500/60 text-white; @apply bg-orange-500/60 text-white;
} }

View File

@@ -1,7 +1,14 @@
# torsearch-app is built from this base, and torsearch-worker & torsearch-scheduler are built from torsearch-app # torsearch-app is built from this base
# This image is used to speed up build times export APP_FRANKENPHP_TAG=php8.4
export FRANKENPHP_TAG=php8.4
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base -t code.caldwell.digital/home/torsearch-base:${FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${FRANKENPHP_TAG}" . docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base:${FRANKENPHP_TAG} docker push code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base:latest docker push code.caldwell.digital/home/torsearch-base:latest
# torsearch-worker & torsearch-scheduler are built from this base
export WORKER_FRANKENPHP_TAG=php8.4-alpine
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base-worker:latest

View File

@@ -33,7 +33,11 @@ services:
worker: worker:
build: . build:
dockerfile: docker/Dockerfile.base.worker
context: .
args:
FRANKENPHP_TAG: php8.4-alpine
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- $PWD:/app - $PWD:/app
@@ -41,11 +45,15 @@ services:
tty: true tty: true
environment: environment:
TZ: America/Chicago TZ: America/Chicago
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 command: php /app/bin/console messenger:consume async --time-limit=3600 -vv
scheduler: scheduler:
build: . build:
dockerfile: docker/Dockerfile.base.worker
context: .
args:
FRANKENPHP_TAG: php8.4-alpine
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- $PWD:/app - $PWD:/app

View File

@@ -42,9 +42,20 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
$monitor = $this->monitorRepository->find($command->movieMonitorId); $monitor = $this->monitorRepository->find($command->movieMonitorId);
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode()); $this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode()); $episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) { if (null === $episodeData->episodeAirDate || "" !== $episodeData->episodeAirDate) {
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode does not have an air date, skipping for now');
return new MonitorTvEpisodeResult(
status: 'OK',
result: [
'message' => 'No change',
'monitor' => $monitor,
]
);
}
if (null === $monitor->getAirDate()) {
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate)); $monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
} }

View File

@@ -170,13 +170,13 @@ class TmdbClient
return $result; return $result;
} }
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
{ {
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']); $result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
return $this->parseResult( return $this->parseResult(
$result, $result,
MediaType::TvEpisode->value, MediaType::TvEpisode->value,
$result['external_ids']['imdb_id'] $showImdbId
); );
} }

View File

@@ -24,7 +24,7 @@ class GetTvShowOptionsHandler implements HandlerInterface
public function handle(CommandInterface $command): ResultInterface public function handle(CommandInterface $command): ResultInterface
{ {
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->season, $command->episode); $media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->imdbId, $command->season, $command->episode);
$parentShow = $this->tmdb->tvshowDetails($command->imdbId); $parentShow = $this->tmdb->tvshowDetails($command->imdbId);
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode); $file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);

View File

@@ -39,15 +39,9 @@
</div> </div>
{{ form_end(preferences_form) }} {{ form_end(preferences_form) }}
<div class="flex flex-col md:flex-row justify-between"> <div class="flex flex-col-reverse md:flex-row justify-between">
<span
{{ stimulus_target('result-filter', 'loadingIcon') }}
{{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
class="loading-icon">
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
</span>
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}
<p class="ml-2 mt-3 md:[margin-top:unset] md:self-center">Season <span data-result-filter-target="currentSeason" class="current-season">{{ results.season }}</span></p>
<div class="flex flex-row gap-2 justify-end px-8"> <div class="flex flex-row gap-2 justify-end px-8">
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 border border-green-500 bg-green-800/60 rounded-ms text-sm font-semibold" show_cancel show_submit> <twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 border border-green-500 bg-green-800/60 rounded-ms text-sm font-semibold" show_cancel show_submit>
Downloading an entire season this way will use the filter from your Downloading an entire season this way will use the filter from your

View File

@@ -14,18 +14,10 @@
> >
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate"> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}" <a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
class="mr-1 hover:underline rounded-md" class="mr-1 hover:underline rounded-md">
> {% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type, season: episodeIdDto.season} %}
{% if monitor.monitorType == "movies" %} <a href="{{ path('app_search_result', routeParams) }}"
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType} %}
{% set route = path('app_search_result', routeParams) %}
{% else %}
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType, season: episodeIdDto.season, episode: episodeIdDto.episode} %}
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
{% endif %}
<a href="{{ route }}"
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate dark:text-white"> class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate dark:text-white">
{{ monitor.title }} {{ monitor.title }}
</a> </a>

View File

@@ -5,7 +5,6 @@
{% for episode in this.getEpisodes().items %} {% for episode in this.getEpisodes().items %}
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results" <episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
show-title="{{ this.title }}" show-title="{{ this.title }}"
data-tv-results-loading-icon-outlet=".loading-icon"
data-download-button-outlet=".download-btn" data-download-button-outlet=".download-btn"
{{ stimulus_controller('tv_results', { {{ stimulus_controller('tv_results', {
title: this.title, title: this.title,
@@ -32,7 +31,7 @@
<p>{{ episode.description|truncate }}</p> <p>{{ episode.description|truncate }}</p>
<div class="text-xs font-bold"> <div class="text-xs font-bold">
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}."> <button class="results-count-badge py-1 px-1.5 mr-1 grow-0 bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results <span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}></span> results
</button> </button>
<span class="py-1 px-1.5 mr-1 grow-0 bg-gray-700 rounded-lg text-white" title='"{{ episode.name }}" aired on {{ episode.airDate|date(null, 'UTC') }}.'> <span class="py-1 px-1.5 mr-1 grow-0 bg-gray-700 rounded-lg text-white" title='"{{ episode.name }}" aired on {{ episode.airDate|date(null, 'UTC') }}.'>
@@ -72,14 +71,16 @@
</div> </div>
</div> </div>
<div class="results-container inline-block overflow-hidden rounded-lg hidden"> <div class="results-container inline-block overflow-hidden rounded-lg hidden">
<twig:Turbo:Frame id="results_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('app_torrentio_tvshows', { <twig:Turbo:Frame loading="lazy" id="results_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('app_torrentio_tvshows', {
tmdbId: this.tmdbId, tmdbId: this.tmdbId,
imdbId: this.imdbId, imdbId: this.imdbId,
season: episode.seasonNumber, season: episode.seasonNumber,
episode: episode.episodeNumber, episode: episode.episodeNumber,
target: 'results_' ~ episode_id(episode.seasonNumber, episode.episodeNumber), target: 'results_' ~ episode_id(episode.seasonNumber, episode.episodeNumber),
block: 'tvshow_results' block: 'tvshow_results'
}) }}" /> }) }}">
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
</twig:Turbo:Frame>
</div> </div>
</div> </div>
</episode-container> </episode-container>

View File

@@ -19,7 +19,7 @@
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline"> <a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" /> <twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
</a> </a>
<div id="calendar" class="text-white"> <div id="upcoming_episodes_calendar" class="text-white">
</div> </div>
</twig:Card> </twig:Card>
</div> </div>
@@ -33,15 +33,22 @@
} }
} }
document.addEventListener('DOMContentLoaded', async function() { document.addEventListener('DOMContentLoaded', async function() {
const modal = document.getElementById('previewModal');
let data = await fetch('/api/monitor/upcoming-episodes'); let data = await fetch('/api/monitor/upcoming-episodes');
data = (await data.json())['data']; data = (await data.json())['data'];
const calendarEl = document.getElementById('calendar'); const calendarEl = document.getElementById('upcoming_episodes_calendar');
const calendar = new FullCalendar.Calendar(calendarEl, { const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: getView(), initialView: getView(),
events: data['episodes'], events: data['episodes'],
windowResize: function(arg) { windowResize: function(arg) {
this.changeView(getView()); this.changeView(getView());
},
eventClick: function (data) {
modal.display({
heading: data.event.title,
content: `<p>${data.event.startStr}</p>`
})
} }
}); });
calendar.render(); calendar.render();

View File

@@ -83,10 +83,10 @@
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}
<div class="flex flex-row justify-start items-end grow text-xs"> <div class="flex flex-row justify-start items-end grow text-xs">
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"> <span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
<span>{{ results.media.numberSeasons }}</span> season(s) <span>{{ results.media.numberSeasons }}</span> season(s)
</span> </span>
<span class="py-1 px-1.5 mr-1 grow-0 bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'> <span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
{{ results.media.premiereDate|date(null, 'UTC') }} {{ results.media.premiereDate|date(null, 'UTC') }}
</span> </span>
</div> </div>
@@ -125,14 +125,15 @@
{% if "movies" == results.media.mediaType %} {% if "movies" == results.media.mediaType %}
<movie-container class="results" <movie-container class="results"
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }} {{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
data-movie-results-loading-icon-outlet=".loading-icon"
> >
<twig:Turbo:Frame id="movie_results_frame" src="{{ path('app_torrentio_movies', { <twig:Turbo:Frame id="movie_results_frame" src="{{ path('app_torrentio_movies', {
tmdbId: results.media.tmdbId, tmdbId: results.media.tmdbId,
imdbId: results.media.imdbId, imdbId: results.media.imdbId,
target: 'movie_results_frame', target: 'movie_results_frame',
block: 'movie_results' block: 'movie_results'
}) }}" /> }) }}">
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" title="Loading download options for {{ results.media.title }}" />
</twig:Turbo:Frame>
</movie-container> </movie-container>
{% elseif "tvshows" == results.media.mediaType %} {% elseif "tvshows" == results.media.mediaType %}
<twig:TvEpisodeList <twig:TvEpisodeList
@@ -151,8 +152,6 @@
</div> </div>
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white"> <twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full"> <div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
{% for media in results.relatedMedia %} {% for media in results.relatedMedia %}
<twig:Poster imdbId="{{ media.imdbId }}" <twig:Poster imdbId="{{ media.imdbId }}"