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 MovieContainer from "./components/movie-container.js";
import StatusCheckerSpan from "./components/status-checker-span.js";
import DownloadMediaButton from "./components/download-media-buton.js";
import { startStimulusApp } from '@symfony/stimulus-bundle';
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('monitor-list-row', MonitorListRow, {extends: 'tr'});
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);
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->setTitle($media->media->title);
$download->setFileName(
@@ -120,7 +133,6 @@ readonly class DownloadMediaHandler implements HandlerInterface
if ($download->getStatus() !== 'Paused') {
$this->downloadRepository->updateStatus($download->getId(), 'Complete');
}
} catch (\Throwable $exception) {
throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500);
}
@@ -156,38 +168,42 @@ readonly class DownloadMediaHandler implements HandlerInterface
{
$fileType = $option->ptn->container;
return (match ($mediaType) {
MediaType::Movie => function () use ($media, $fileType) {
$template = "%s (%s) [imdbid-%s].%s";
return sprintf(
MediaType::Movie => function () use ($media, $fileType, $option) {
$template = "%s (%s) [imdbid-%s]";
$filename = sprintf(
$template,
$media->media->title,
$media->media->year,
$media->media->imdbId,
$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";
$episodeId = EpisodeId::fromSeasonEpisodeNumbers(
$media->season,
$media->episode,
);
return sprintf(
$filename = sprintf(
$template,
$media->media->title,
$episodeId,
$fileType
);
if ($option->resolution !== null && $option->resolution !== '-') {
$filename .= ' - [' . $option->resolution . ']';
}
return $filename . '.' . $fileType;
}
})();
}
public function validateDownloadUrl(string $downloadUrl)
{
$badFileSizes = [
2119075, // copyright infringement
];
$badFileLocations = [
'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.'

View File

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

View File

@@ -16,6 +16,8 @@ use App\Download\DownloadEvents;
use App\Download\Framework\Entity\Download;
use App\Download\Framework\Repository\DownloadRepository;
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\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
@@ -30,15 +32,18 @@ class ApiController extends AbstractController
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(
DownloadMediaInput $input,
DownloadMediaHandler $handler,
GetMediaInfoHandler $getMediaInfoHandler,
): Response {
$media = $getMediaInfoHandler->handle(new GetMediaInfoCommand($input->imdbId, $input->mediaType));
$download = $this->downloadRepository->insertNew(
$this->getUser(),
$input->imdbId,
$input->mediaType,
$media->media->title,
$input->season,
$input->episode,
);

View File

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

View File

@@ -9,7 +9,7 @@
{% set preferences_form = 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">
{{ form_row(preferences_form.resolution) }}
{{ form_row(preferences_form.codec) }}
@@ -37,6 +37,17 @@
</div>
{% endif %}
</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) }}
<div class="flex flex-col-reverse md:flex-row justify-between">

View File

@@ -53,11 +53,21 @@
</div>
</div>
<div class="flex flex-col gap-4 justify-between">
<div class="flex flex-col items-center">
<input class="episode-selector" type="checkbox"
{{ stimulus_target('tv-results', 'episodeSelector') }}
/>
</div>
<download-media-button
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"
title="Download season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }} of {{ this.title }}"
media-type="tvshows"
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 }}.">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
<path

View File

@@ -19,9 +19,11 @@
<div class="w-full flex flex-col">
<div class="mb-4 flex flex-row gap-2 justify-between">
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
{{ results.media.title }} ({{ results.media.year }})
</h3>
<div class="flex flex-row justify-between items-center gap-2 w-full">
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
{{ results.media.title }} ({{ results.media.year }})
</h3>
</div>
{% if results.media.mediaType == "tvshows" %}
<div {{ stimulus_controller('monitor_button', {
@@ -96,9 +98,9 @@
{% if "movies" == results.media.mediaType %}
<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-number" id="movie_results_count">-</span> results
</span>
{# <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>#}
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
title: results.media.title,
@@ -133,18 +135,18 @@
<twig:Filter results="{{ results }}" filter="{{ filter }}" />
{% if "movies" == results.media.mediaType %}
<movie-container class="results"
{{ 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', {
tmdbId: results.media.tmdbId,
imdbId: results.media.imdbId,
target: 'movie_results_frame',
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 class="results"#}
{# {{ 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', {#}
{# tmdbId: results.media.tmdbId,#}
{# imdbId: results.media.imdbId,#}
{# target: 'movie_results_frame',#}
{# 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>#}
{% elseif "tvshows" == results.media.mediaType %}
<twig:TvEpisodeList
results="results"