WIP: workig download button on movies and episodes

This commit is contained in:
Brock H Caldwell
2026-03-22 23:31:03 -05:00
parent e39cb6e9bd
commit 740bef97b1
9 changed files with 150 additions and 39 deletions

2
assets/bootstrap.js vendored
View File

@@ -6,6 +6,7 @@ import DownloadListRow from './components/download-list-row.js';
import MonitorListRow from './components/monitor-list-row.js'; import MonitorListRow from './components/monitor-list-row.js';
import MovieContainer from "./components/movie-container.js"; import MovieContainer from "./components/movie-container.js";
import StatusCheckerSpan from "./components/status-checker-span.js"; import StatusCheckerSpan from "./components/status-checker-span.js";
import DownloadMediaButton from "./components/download-media-buton.js";
import { startStimulusApp } from '@symfony/stimulus-bundle'; import { startStimulusApp } from '@symfony/stimulus-bundle';
import Popover from '@stimulus-components/popover'; import Popover from '@stimulus-components/popover';
@@ -26,3 +27,4 @@ customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'}); customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'}); customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'});
customElements.define('status-checker-span', StatusCheckerSpan, {extends: 'span'}); customElements.define('status-checker-span', StatusCheckerSpan, {extends: 'span'});
customElements.define('download-media-button', DownloadMediaButton);

View File

@@ -0,0 +1,58 @@
export default class DownloadMediaButon extends HTMLElement
{
#filterEl;
#mediaType;
#imdbId;
#season = null;
#episode = null;
connectedCallback() {
this.#filterEl = this.querySelector('#filter');
this.#mediaType = this.getAttribute('media-type');
this.#imdbId = this.getAttribute('imdb-id');
if (this.getAttribute('season') !== null && this.getAttribute('episode') !== null) {
this.#season = this.getAttribute('season');
this.#episode = this.getAttribute('episode');
} else if (this.#mediaType === 'tvshows') {
console.warn('Season and Episode are not set for \'tvshows\' media type');
}
this.addEventListener('click', this.download.bind(this));
}
disconnectedCallback() {
this.removeEventListener('click', this.download.bind(this));
}
connectedMoveCallback() {}
download() {
const preferencesForm = document.querySelector('[name="user_media_preferences_form"]');
const preferences = {
resolution: preferencesForm.querySelector('[id="user_media_preferences_form_resolution"]').value,
codec: preferencesForm.querySelector('[id="user_media_preferences_form_codec"]').value,
language: preferencesForm.querySelector('[id="user_media_preferences_form_language"]').value,
quality: preferencesForm.querySelector('[id="user_media_preferences_form_quality"]').value,
provider: preferencesForm.querySelector('[id="user_media_preferences_form_provider"]').value,
}
console.log(preferences);
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
mediaType: this.#mediaType,
imdbId: this.#imdbId,
season: this.#season,
episode: this.#episode,
filter: preferences,
})
})
.then(res => res.json())
.then(json => {
console.log(json)
})
}
}

View File

@@ -82,6 +82,19 @@ readonly class DownloadMediaHandler implements HandlerInterface
$matchingOption = $this->downloadOptionEvaluator->evaluateOptions($downloadOptions->results, $filter); $matchingOption = $this->downloadOptionEvaluator->evaluateOptions($downloadOptions->results, $filter);
if ($matchingOption === null) {
$download->setProgress(100);
$download->setStatus('Failed');
$this->downloadRepository->getEntityManager()->flush();
$this->broadcaster->alert(
title:'Uh oh',
message: 'No matching download options found.',
type: 'warning',
mercureAlertTopic: $command->mercureAlertTopic
);
throw new UnrecoverableMessageHandlingException('No matching download options found.', 404);
}
$download->setUrl($matchingOption->url); $download->setUrl($matchingOption->url);
$download->setTitle($media->media->title); $download->setTitle($media->media->title);
$download->setFileName( $download->setFileName(
@@ -120,7 +133,6 @@ readonly class DownloadMediaHandler implements HandlerInterface
if ($download->getStatus() !== 'Paused') { if ($download->getStatus() !== 'Paused') {
$this->downloadRepository->updateStatus($download->getId(), 'Complete'); $this->downloadRepository->updateStatus($download->getId(), 'Complete');
} }
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500); throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500);
} }
@@ -156,38 +168,42 @@ readonly class DownloadMediaHandler implements HandlerInterface
{ {
$fileType = $option->ptn->container; $fileType = $option->ptn->container;
return (match ($mediaType) { return (match ($mediaType) {
MediaType::Movie => function () use ($media, $fileType) { MediaType::Movie => function () use ($media, $fileType, $option) {
$template = "%s (%s) [imdbid-%s].%s"; $template = "%s (%s) [imdbid-%s]";
return sprintf( $filename = sprintf(
$template, $template,
$media->media->title, $media->media->title,
$media->media->year, $media->media->year,
$media->media->imdbId, $media->media->imdbId,
$fileType $fileType
); );
if ($option->resolution !== null && $option->resolution !== '-') {
$filename .= ' - [' . $option->resolution . ']';
}
return $filename . '.' . $fileType;
}, },
MediaType::TvShow => function () use ($media, $fileType) { MediaType::TvShow => function () use ($media, $fileType, $option) {
$template = "%s %s.%s"; $template = "%s %s.%s";
$episodeId = EpisodeId::fromSeasonEpisodeNumbers( $episodeId = EpisodeId::fromSeasonEpisodeNumbers(
$media->season, $media->season,
$media->episode, $media->episode,
); );
return sprintf( $filename = sprintf(
$template, $template,
$media->media->title, $media->media->title,
$episodeId, $episodeId,
$fileType $fileType
); );
if ($option->resolution !== null && $option->resolution !== '-') {
$filename .= ' - [' . $option->resolution . ']';
}
return $filename . '.' . $fileType;
} }
})(); })();
} }
public function validateDownloadUrl(string $downloadUrl) public function validateDownloadUrl(string $downloadUrl)
{ {
$badFileSizes = [
2119075, // copyright infringement
];
$badFileLocations = [ $badFileLocations = [
'https://torrentio.strem.fun/videos/failed_infringement_v2.mp4' => 'Removed for Copyright Infringement.', 'https://torrentio.strem.fun/videos/failed_infringement_v2.mp4' => 'Removed for Copyright Infringement.',
'https://torrentio.strem.fun/videos/downloading_v2.mp4' => 'Your torrent is downloading to your debrid provider.' 'https://torrentio.strem.fun/videos/downloading_v2.mp4' => 'Your torrent is downloading to your debrid provider.'

View File

@@ -5,6 +5,7 @@ namespace App\Download\Action\Input;
use App\Download\Action\Command\DownloadMediaCommand; use App\Download\Action\Command\DownloadMediaCommand;
use OneToMany\RichBundle\Attribute\PropertyIgnored; use OneToMany\RichBundle\Attribute\PropertyIgnored;
use OneToMany\RichBundle\Attribute\SourceRequest; use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Attribute\SourceSecurity; use OneToMany\RichBundle\Attribute\SourceSecurity;
use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface; use OneToMany\RichBundle\Contract\InputInterface;
@@ -17,15 +18,19 @@ class DownloadMediaInput implements InputInterface
public function __construct( public function __construct(
#[SourceRequest('imdbId')] #[SourceRequest('imdbId')]
#[SourceRoute('imdbId')]
public string $imdbId, public string $imdbId,
#[SourceRequest('mediaType')] #[SourceRequest('mediaType')]
#[SourceRoute('mediaType')]
public string $mediaType, public string $mediaType,
#[SourceRequest('season', nullify: true)] #[SourceRequest('season')]
#[SourceRoute('season')]
public int|string|null $season = null, public int|string|null $season = null,
#[SourceRequest('episode', nullify: true)] #[SourceRequest('episode')]
#[SourceRoute('episode')]
public int|string|null $episode = null, public int|string|null $episode = null,
#[SourceRequest('url', nullify: true)] #[SourceRequest('url', nullify: true)]
@@ -64,8 +69,8 @@ class DownloadMediaInput implements InputInterface
return new DownloadMediaCommand( return new DownloadMediaCommand(
$this->imdbId, $this->imdbId,
$this->mediaType, $this->mediaType,
$this->season, (int) $this->season,
$this->episode, (int) $this->episode,
$this->url, $this->url,
$this->filter, $this->filter,
$this->downloadId, $this->downloadId,

View File

@@ -16,6 +16,8 @@ use App\Download\DownloadEvents;
use App\Download\Framework\Entity\Download; use App\Download\Framework\Entity\Download;
use App\Download\Framework\Repository\DownloadRepository; use App\Download\Framework\Repository\DownloadRepository;
use App\EventLog\Action\Command\AddEventLogCommand; use App\EventLog\Action\Command\AddEventLogCommand;
use App\Search\Action\Command\GetMediaInfoCommand;
use App\Search\Action\Handler\GetMediaInfoHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -30,15 +32,18 @@ class ApiController extends AbstractController
private readonly Broadcaster $broadcaster, private readonly RequestStack $requestStack, private readonly Broadcaster $broadcaster, private readonly RequestStack $requestStack,
) {} ) {}
#[Route('/api/download', name: 'api_download', methods: ['POST'])] #[Route('/api/download/{mediaType?}/{imdbId?}/{season?}/{episode?}', name: 'api_download', methods: ['GET', 'POST'])]
public function download( public function download(
DownloadMediaInput $input, DownloadMediaInput $input,
DownloadMediaHandler $handler, DownloadMediaHandler $handler,
GetMediaInfoHandler $getMediaInfoHandler,
): Response { ): Response {
$media = $getMediaInfoHandler->handle(new GetMediaInfoCommand($input->imdbId, $input->mediaType));
$download = $this->downloadRepository->insertNew( $download = $this->downloadRepository->insertNew(
$this->getUser(), $this->getUser(),
$input->imdbId, $input->imdbId,
$input->mediaType, $input->mediaType,
$media->media->title,
$input->season, $input->season,
$input->episode, $input->episode,
); );

View File

@@ -61,6 +61,7 @@ class DownloadRepository extends ServiceEntityRepository
UserInterface $user, UserInterface $user,
string $imdbId, string $imdbId,
string $mediaType, string $mediaType,
string $title,
int|null $season = null, int|null $season = null,
int|null $episode = null, int|null $episode = null,
string $status = 'New' string $status = 'New'
@@ -69,6 +70,7 @@ class DownloadRepository extends ServiceEntityRepository
$download = (new Download()) $download = (new Download())
->setUser($user) ->setUser($user)
->setImdbId($imdbId) ->setImdbId($imdbId)
->setTitle($title)
->setMediaType($mediaType) ->setMediaType($mediaType)
->setProgress(0) ->setProgress(0)
->setStatus($status); ->setStatus($status);

View File

@@ -9,7 +9,7 @@
{% set preferences_form = form %} {% set preferences_form = form %}
{{ form_start(preferences_form) }} {{ form_start(preferences_form) }}
<h3 class="font-bold text-lg mb-2 md:mb-4">Apply a filter to your results</h3> <h3 class="font-bold text-lg mb-2 md:mb-4">What type of file do you want?</h3>
<div class="flex flex-col md:flex-row gap-2 justify-between"> <div class="flex flex-col md:flex-row gap-2 justify-between">
{{ form_row(preferences_form.resolution) }} {{ form_row(preferences_form.resolution) }}
{{ form_row(preferences_form.codec) }} {{ form_row(preferences_form.codec) }}
@@ -37,6 +37,17 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if results.media.mediaType == "movies" %}
<div class="w-full flex-col md:flex-row justify-between gap-2">
<download-media-button
class="px-2 py-1 bg-green-500 bg-opacity-60 font-medium rounded-lg text-sm cursor-pointer self-end"
title="Download {{ results.media.title }}"
media-type="{{ results.media.mediaType }}"
imdb-id="{{ results.media.imdbId }}">
download
</download-media-button>
</div>
{% endif %}
{{ form_end(preferences_form) }} {{ form_end(preferences_form) }}
<div class="flex flex-col-reverse md:flex-row justify-between"> <div class="flex flex-col-reverse md:flex-row justify-between">

View File

@@ -53,11 +53,21 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-4 justify-between"> <div class="flex flex-col gap-4 justify-between">
<div class="flex flex-col items-center"> <download-media-button
<input class="episode-selector" type="checkbox" class="px-2 py-2 bg-green-500 bg-opacity-80 font-medium text-center rounded-lg text-sm cursor-pointer items-center self-end"
{{ stimulus_target('tv-results', 'episodeSelector') }} title="Download season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }} of {{ this.title }}"
/> media-type="tvshows"
</div> imdb-id="{{ this.imdbId }}"
season="{{ episode.seasonNumber }}"
episode="{{ episode.episodeNumber }}"
>
<twig:ux:icon name="bi:download" width="20" />
</download-media-button>
<input class="episode-selector" type="checkbox"
{{ stimulus_target('tv-results', 'episodeSelector') }}
/>
<button class="dropdown-button flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}."> <button class="dropdown-button flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
<path <path

View File

@@ -19,9 +19,11 @@
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="mb-4 flex flex-row gap-2 justify-between"> <div class="mb-4 flex flex-row gap-2 justify-between">
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50"> <div class="flex flex-row justify-between items-center gap-2 w-full">
{{ results.media.title }} ({{ results.media.year }}) <h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
</h3> {{ results.media.title }} ({{ results.media.year }})
</h3>
</div>
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}
<div {{ stimulus_controller('monitor_button', { <div {{ stimulus_controller('monitor_button', {
@@ -96,9 +98,9 @@
{% if "movies" == results.media.mediaType %} {% if "movies" == results.media.mediaType %}
<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="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"> {# <span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">#}
<span class="results-count-number" id="movie_results_count">-</span> results {# <span class="results-count-number" id="movie_results_count">-</span> results#}
</span> {# </span>#}
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', { <twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
title: results.media.title, title: results.media.title,
@@ -133,18 +135,18 @@
<twig:Filter results="{{ results }}" filter="{{ filter }}" /> <twig:Filter results="{{ results }}" filter="{{ filter }}" />
{% 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}) }}#}
> {# >#}
<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: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> {# </twig:Turbo:Frame>#}
</movie-container> {# </movie-container>#}
{% elseif "tvshows" == results.media.mediaType %} {% elseif "tvshows" == results.media.mediaType %}
<twig:TvEpisodeList <twig:TvEpisodeList
results="results" results="results"