Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c09c7ad030 | |||
| f610297294 | |||
| f2971eee9c | |||
| 47108af1f8 | |||
| f7163b5e00 | |||
| 31e364d691 | |||
| b42981b2a1 | |||
| accfa9c9bf | |||
| 8b50b50466 | |||
| e38498f69b | |||
| 490f341875 | |||
| b1b28864ea |
@@ -20,24 +20,19 @@ export default class extends Controller {
|
||||
|
||||
options = []
|
||||
optionsLoaded = false
|
||||
resultCountEl = null
|
||||
|
||||
async connect() {
|
||||
await this.setOptions();
|
||||
this.resultCountEl = document.querySelector('#movie_results_count');
|
||||
}
|
||||
|
||||
async setOptions() {
|
||||
if (false === this.optionsLoaded) {
|
||||
this.optionsLoaded = true;
|
||||
await fetch(`/torrentio/movies/${this.tmdbIdValue}/${this.imdbIdValue}`)
|
||||
.then(res => res.text())
|
||||
.then(response => {
|
||||
this.element.innerHTML = response;
|
||||
this.options = this.element.querySelectorAll('tbody tr');
|
||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||
this.loadingIconOutlet.toggleIcon();
|
||||
});
|
||||
}
|
||||
async listTargetConnected() {
|
||||
this.optionsLoaded = true;
|
||||
this.options = this.element.querySelectorAll('tbody tr');
|
||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||
this.loadingIconOutlet.toggleIcon();
|
||||
this.resultCountEl.innerText = this.options.length;
|
||||
}
|
||||
|
||||
// Keeps compatible with Filter & TV Shows
|
||||
@@ -102,5 +97,6 @@ export default class extends Controller {
|
||||
count = count + 1;
|
||||
}
|
||||
});
|
||||
this.resultCountEl.innerText = count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,37 +25,18 @@ export default class extends Controller {
|
||||
optionsLoaded = false
|
||||
isOpen = false
|
||||
|
||||
async connect() {
|
||||
await this.setOptions();
|
||||
}
|
||||
|
||||
async setOptions() {
|
||||
if (this.optionsLoaded === false) {
|
||||
this.optionsLoaded = true;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||
} catch (error) {
|
||||
console.log('There was an error', error);
|
||||
}
|
||||
|
||||
if (response?.ok) {
|
||||
response = await response.text()
|
||||
this.listContainerTarget.innerHTML = response;
|
||||
this.options = this.element.querySelectorAll('tbody tr');
|
||||
if (this.options.length > 0) {
|
||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||
this.options[0].querySelector('input[type="checkbox"]').checked = true;
|
||||
} else {
|
||||
this.countTarget.innerText = 0;
|
||||
this.episodeSelectorTarget.disabled = true;
|
||||
}
|
||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||
this.loadingIconOutlet.increaseCount();
|
||||
} else {
|
||||
console.log(`HTTP Response Code: ${response?.status}`)
|
||||
}
|
||||
async listTargetConnected() {
|
||||
this.options = this.element.querySelectorAll('tbody tr');
|
||||
if (this.options.length > 0) {
|
||||
this.options.forEach((option) =>
|
||||
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
||||
);
|
||||
this.options[0].querySelector('input[type="checkbox"]').checked = true;
|
||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||
this.loadingIconOutlet.increaseCount();
|
||||
} else {
|
||||
this.countTarget.innerText = 0;
|
||||
this.episodeSelectorTarget.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +115,7 @@ export default class extends Controller {
|
||||
"codec": option.querySelector('#codec').textContent.trim(),
|
||||
"provider": option.querySelector('#provider').textContent.trim(),
|
||||
"languages": JSON.parse(option.dataset['languages']),
|
||||
"quality": option.dataset['quality'],
|
||||
}
|
||||
|
||||
let include = true;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
dev.caldwell.digital:443
|
||||
{
|
||||
log {
|
||||
level DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
|
||||
|
||||
reverse_proxy app:80
|
||||
dev.caldwell.digital:443 {
|
||||
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
|
||||
|
||||
reverse_proxy app:80
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ services:
|
||||
caddy:
|
||||
image: caddy:2.9.1
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
|
||||
@@ -6,8 +6,8 @@ pwa:
|
||||
start_url: "/"
|
||||
display: "standalone"
|
||||
id: "/"
|
||||
background_color: "#083344"
|
||||
theme_color: "#f98e44"
|
||||
background_color: "#f98e44"
|
||||
theme_color: "#083344"
|
||||
description: Torsearch provides a simple and intuitive way to manage your personal media library.
|
||||
icons:
|
||||
- src: "icon.png"
|
||||
|
||||
@@ -32,6 +32,8 @@ security:
|
||||
login_path: '/login/oidc'
|
||||
check_path: '/login/oidc/auth'
|
||||
enable_end_session_listener: true
|
||||
http_basic:
|
||||
realm: Secured Area
|
||||
entry_point: form_login
|
||||
|
||||
# activate different ways to authenticate
|
||||
|
||||
@@ -6,6 +6,14 @@ controllersBase:
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersLibrary:
|
||||
resource:
|
||||
path: ../src/Library/Framework/Controller/
|
||||
namespace: App\Library\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersSearch:
|
||||
resource:
|
||||
path: ../src/Search/Framework/Controller/
|
||||
|
||||
@@ -140,7 +140,7 @@ class MediaFiles
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber)
|
||||
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber): SplFileInfo|false
|
||||
{
|
||||
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||
|
||||
|
||||
16
src/Library/Action/Command/LibrarySearchCommand.php
Normal file
16
src/Library/Action/Command/LibrarySearchCommand.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class LibrarySearchCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $term = null,
|
||||
public ?string $title = null,
|
||||
public ?string $imdbId = null,
|
||||
public ?string $season = null,
|
||||
public ?string $episode = null,
|
||||
) {}
|
||||
}
|
||||
83
src/Library/Action/Handler/LibrarySearchHandler.php
Normal file
83
src/Library/Action/Handler/LibrarySearchHandler.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Action\Handler;
|
||||
|
||||
use App\Base\Service\MediaFiles;
|
||||
use App\Library\Action\Command\LibrarySearchCommand;
|
||||
use App\Library\Action\Result\LibrarySearchResult;
|
||||
use App\Library\Dto\MediaFileDto;
|
||||
use Nihilarr\PTN;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/**
|
||||
* @implements HandlerInterface<LibrarySearchCommand,LibrarySearchHandler>
|
||||
*/
|
||||
class LibrarySearchHandler implements HandlerInterface
|
||||
{
|
||||
private array $searchTypes = [
|
||||
'episode_by_title' => 'episodeByTitle',
|
||||
'movie_by_title' => 'movieByTitle',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly MediaFiles $mediaFiles,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$searchType = $this->getSearchType($command);
|
||||
$function = $this->searchTypes[$searchType];
|
||||
return $this->$function($command);
|
||||
}
|
||||
|
||||
private function getSearchType(CommandInterface $command): ?string
|
||||
{
|
||||
if (!is_null($command->title) &&
|
||||
is_null($command->imdbId) &&
|
||||
is_null($command->season) &&
|
||||
is_null($command->episode)
|
||||
) {
|
||||
return 'movie_by_title';
|
||||
}
|
||||
|
||||
if ((!is_null($command->title) || is_null($command->imdbId)) &&
|
||||
!is_null($command->season) &&
|
||||
!is_null($command->episode)
|
||||
) {
|
||||
return 'episode_by_title';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function episodeByTitle(CommandInterface $command): ?LibrarySearchResult
|
||||
{
|
||||
$result = $this->mediaFiles->episodeExists(
|
||||
$command->title,
|
||||
(int) $command->season,
|
||||
(int) $command->episode,
|
||||
);
|
||||
$exists = $result instanceof \SplFileInfo;
|
||||
|
||||
return new LibrarySearchResult(
|
||||
input: $command,
|
||||
exists: $exists,
|
||||
file: true === $exists ? MediaFileDto::fromSplFileInfo($result) : null,
|
||||
ptn: true === $exists ? (object) new PTN()->parse($result->getFilename()) : null,
|
||||
);
|
||||
}
|
||||
|
||||
private function movieByTitle(CommandInterface $command): ?LibrarySearchResult
|
||||
{
|
||||
$result = $this->mediaFiles->movieExists($command->title);
|
||||
$exists = $result instanceof \SplFileInfo;
|
||||
|
||||
return new LibrarySearchResult(
|
||||
input: $command,
|
||||
exists: $exists,
|
||||
file: true === $exists ? MediaFileDto::fromSplFileInfo($result) : null,
|
||||
ptn: true === $exists ? (object) new PTN()->parse($result->getFilename()) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/Library/Action/Input/LibrarySearchInput.php
Normal file
38
src/Library/Action/Input/LibrarySearchInput.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Action\Input;
|
||||
|
||||
use App\Library\Action\Command\LibrarySearchCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceQuery;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/**
|
||||
* @implements InputInterface<LibrarySearchInput, LibrarySearchCommand>
|
||||
*/
|
||||
class LibrarySearchInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceQuery('term', nullify: true)]
|
||||
private ?string $term = null,
|
||||
#[SourceQuery('title', nullify: true)]
|
||||
private ?string $title = null,
|
||||
#[SourceQuery('imdbId', nullify: true)]
|
||||
private ?string $imdbId = null,
|
||||
#[SourceQuery('season', nullify: true)]
|
||||
private ?string $season = null,
|
||||
#[SourceQuery('episode', nullify: true)]
|
||||
private ?string $episode = null,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new LibrarySearchCommand(
|
||||
term: $this->term,
|
||||
title: $this->title,
|
||||
imdbId: $this->imdbId,
|
||||
season: $this->season,
|
||||
episode: $this->episode,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Library/Action/Result/LibrarySearchResult.php
Normal file
16
src/Library/Action/Result/LibrarySearchResult.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Action\Result;
|
||||
|
||||
use App\Library\Dto\MediaFileDto;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class LibrarySearchResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public object|array $input,
|
||||
public bool $exists,
|
||||
public ?MediaFileDto $file = null,
|
||||
public ?object $ptn = null,
|
||||
) {}
|
||||
}
|
||||
23
src/Library/Dto/MediaFileDto.php
Normal file
23
src/Library/Dto/MediaFileDto.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Dto;
|
||||
|
||||
readonly class MediaFileDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $path,
|
||||
public string $filename,
|
||||
public string $extension,
|
||||
public string $size,
|
||||
) {}
|
||||
|
||||
public static function fromSplFileInfo(\SplFileInfo $fileInfo): self
|
||||
{
|
||||
return new static(
|
||||
path: $fileInfo->getRealPath(),
|
||||
filename: $fileInfo->getFilename(),
|
||||
extension: $fileInfo->getExtension(),
|
||||
size: $fileInfo->getSize(),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/Library/Framework/Controller/Api.php
Normal file
40
src/Library/Framework/Controller/Api.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Framework\Controller;
|
||||
|
||||
use App\Library\Action\Handler\LibrarySearchHandler;
|
||||
use App\Library\Action\Input\LibrarySearchInput;
|
||||
use App\Library\Action\Result\LibrarySearchResult;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\UX\Turbo\TurboBundle;
|
||||
|
||||
class Api extends AbstractController
|
||||
{
|
||||
#[Route('/api/library/search', name: 'api.library.search', methods: ['GET'])]
|
||||
public function search(LibrarySearchInput $input, LibrarySearchHandler $handler, Request $request): Response
|
||||
{
|
||||
$result = $handler->handle($input->toCommand());
|
||||
|
||||
if ($request->headers->get('Turbo-Frame')) {
|
||||
return $this->sendFragmentResponse($result, $request);
|
||||
}
|
||||
|
||||
return $this->json($handler->handle($input->toCommand()));
|
||||
}
|
||||
|
||||
private function sendFragmentResponse(LibrarySearchResult $result, Request $request): Response
|
||||
{
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
return $this->renderBlock(
|
||||
'search/fragments.html.twig',
|
||||
$request->query->get('block'),
|
||||
[
|
||||
'result' => $result,
|
||||
'target' => $request->query->get('target')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Search\Action\Handler;
|
||||
|
||||
use App\Base\Service\MediaFiles;
|
||||
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||
use App\Search\Action\Result\GetMediaInfoResult;
|
||||
use App\Tmdb\Tmdb;
|
||||
@@ -15,19 +14,12 @@ class GetMediaInfoHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly MediaFiles $mediaFiles
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
|
||||
if ("tvshows" === $command->mediaType) {
|
||||
foreach ($media->episodes[$command->season] as $key => $episode) {
|
||||
$media->episodes[$command->season][$key]['file'] = $this->mediaFiles->episodeExists($media->title, $command->season, $episode['episode_number']);
|
||||
}
|
||||
}
|
||||
|
||||
return new GetMediaInfoResult($media, $command->season);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ final class WebController extends AbstractController
|
||||
public function result(
|
||||
GetMediaInfoInput $input,
|
||||
?int $season = null,
|
||||
): Response {
|
||||
): Response
|
||||
{
|
||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||
|
||||
return $this->render('search/result.html.twig', [
|
||||
@@ -52,32 +53,4 @@ final class WebController extends AbstractController
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function warmDownloadOptionCache(TmdbResult $result)
|
||||
{
|
||||
if ($result->mediaType === 'tvshows') {
|
||||
// dispatches a job to get the download options
|
||||
// for each episode and load them in cache
|
||||
foreach ($result->episodes as $season => $episodes) {
|
||||
// Only do the first 2 seasons, so we reduce
|
||||
// getting rate-limited by Torrentio
|
||||
if ($season > 2) {
|
||||
return;
|
||||
}
|
||||
foreach ($episodes as $episode) {
|
||||
$this->bus->dispatch(new GetTvShowOptionsCommand(
|
||||
tmdbId: $result->tmdbId,
|
||||
imdbId: $result->imdbId,
|
||||
season: $season,
|
||||
episode: $episode['episode_number'],
|
||||
));
|
||||
}
|
||||
}
|
||||
} elseif ($result->mediaType === 'movies') {
|
||||
$this->bus->dispatch(new GetMovieOptionsCommand(
|
||||
$result->tmdbId,
|
||||
$result->imdbId,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ class Tmdb
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||
mediaType: "movies",
|
||||
episodeAirDate: (new \DateTime($data['release_date']))->format('m/d/Y'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||
use Carbon\Carbon;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\UX\Turbo\TurboBundle;
|
||||
|
||||
final class WebController extends AbstractController
|
||||
{
|
||||
@@ -25,7 +27,7 @@ final class WebController extends AbstractController
|
||||
) {}
|
||||
|
||||
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
|
||||
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
|
||||
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache, Request $request): Response
|
||||
{
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.movies.%s.%s",
|
||||
@@ -33,9 +35,14 @@ final class WebController extends AbstractController
|
||||
$input->imdbId
|
||||
);
|
||||
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||
|
||||
if ($request->headers->get('Turbo-Frame')) {
|
||||
return $this->sendFragmentResponse($results, $request);
|
||||
}
|
||||
|
||||
return $this->render('torrentio/movies.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
@@ -43,7 +50,7 @@ final class WebController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
|
||||
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
|
||||
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
|
||||
{
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||
@@ -54,13 +61,18 @@ final class WebController extends AbstractController
|
||||
);
|
||||
|
||||
try {
|
||||
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
|
||||
if ($request->headers->get('Turbo-Frame')) {
|
||||
return $this->sendFragmentResponse($results, $request);
|
||||
}
|
||||
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
// });
|
||||
});
|
||||
} catch (TorrentioRateLimitException $exception) {
|
||||
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning');
|
||||
return $this->render('bare.html.twig',
|
||||
@@ -73,29 +85,16 @@ final class WebController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
|
||||
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
|
||||
private function sendFragmentResponse(ResultInterface $result, Request $request): Response
|
||||
{
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||
$input->tmdbId,
|
||||
$input->imdbId,
|
||||
$input->season,
|
||||
$input->episode,
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
return $this->renderBlock(
|
||||
'torrentio/fragments.html.twig',
|
||||
$request->query->get('block'),
|
||||
[
|
||||
'results' => $result,
|
||||
'target' => $request->query->get('target')
|
||||
]
|
||||
);
|
||||
$cache->delete($cacheId);
|
||||
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: 'Torrentio cache Cleared.'
|
||||
);
|
||||
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class UtilExtension
|
||||
}
|
||||
|
||||
#[AsTwigFilter('episode_id_from_results')]
|
||||
public function episodeId($result): ?string
|
||||
public function episodeIdFromResults($result): ?string
|
||||
{
|
||||
if (!$result instanceof GetTvShowOptionsResult) {
|
||||
return null;
|
||||
@@ -56,4 +56,11 @@ class UtilExtension
|
||||
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
|
||||
"E". str_pad($result->episode, 2, "0", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
#[AsTwigFunction('episode_id')]
|
||||
public function episodeId($season, $episode): ?string
|
||||
{
|
||||
return "S". str_pad($season, 2, "0", STR_PAD_LEFT) .
|
||||
"E". str_pad($episode, 2, "0", STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</a>
|
||||
|
||||
{% if download.mediaType == "tvshows" and download.episodeId != null %}
|
||||
— <span class="ml-1">(S{{ download.episodeId }})</span>
|
||||
— <span class="ml-1">({{ download.episodeId }})</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<header {{ attributes }} class="bg-cyan-950 z-40">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="h-16 flex flex-row items-center justify-between">
|
||||
<a href="{{ path('app_index') }}" class="text-2xl text-orange-500 mr-4 md:hidden">T</a>
|
||||
<a href="{{ path('app_index') }}" class="text-2xl font-extrabold text-orange-500 mr-4 md:hidden">T</a>
|
||||
<twig:SearchBar />
|
||||
<div class="md:flex md:items-center md:gap-12">
|
||||
<nav aria-label="Global" class="md:block">
|
||||
@@ -32,6 +32,9 @@
|
||||
{% for message in app.flashes('warning') %}
|
||||
<twig:Alert :title="'Warning'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
|
||||
{% endfor %}
|
||||
{% for message in app.flashes('success') %}
|
||||
<twig:Alert :title="'Success'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
mediaType: mediaType,
|
||||
imdbId: imdbId
|
||||
}) }}">
|
||||
<img src="{{ image }}" class="w-full md:w-40 rounded-md" />
|
||||
<img src="{{ preload(image) }}" class="w-full md:w-40 rounded-md" />
|
||||
</a>
|
||||
<a href="{{ path('app_search_result', {
|
||||
mediaType: mediaType,
|
||||
imdbId: imdbId
|
||||
}) }}">
|
||||
<h3 class="text-center text-white md:text-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
||||
<h3 class="text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -40,30 +40,17 @@
|
||||
{{ episode['air_date']|date(null, 'UTC') }}
|
||||
</small>
|
||||
|
||||
{% if episode['file'] != false %}
|
||||
<span data-controller="popover">
|
||||
<template data-popover-target="content">
|
||||
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||
<ul class="list-disc ml-3">
|
||||
<li class="font-normal">{{ episode['file'].realPath|strip_media_path }} — <strong>{{ episode['file'].size|filesize }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<small
|
||||
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||
>
|
||||
exists
|
||||
</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if episode['file'] == false %}
|
||||
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
||||
title: this.title,
|
||||
season: episode['season_number'],
|
||||
episode: episode['episode_number'],
|
||||
block: 'media_exists_badge',
|
||||
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
||||
}) }}">
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||
missing
|
||||
</small>
|
||||
{% endif %}
|
||||
</twig:Turbo:Frame>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 justify-between">
|
||||
@@ -87,8 +74,15 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div {{ stimulus_target('tv-results', 'listContainer') }} class="inline-block overflow-hidden rounded-lg">
|
||||
|
||||
<div class="inline-block overflow-hidden rounded-lg">
|
||||
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
|
||||
tmdbId: this.tmdbId,
|
||||
imdbId: this.imdbId,
|
||||
season: episode['season_number'],
|
||||
episode: episode['episode_number'],
|
||||
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
|
||||
block: 'tvshow_results'
|
||||
}) }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
32
templates/search/fragments.html.twig
Normal file
32
templates/search/fragments.html.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
{% block media_exists_badge %}
|
||||
<turbo-stream action="replace" targets="#{{ target }}">
|
||||
<template>
|
||||
{% if result.exists == true %}
|
||||
<span data-controller="popover">
|
||||
<template data-popover-target="content">
|
||||
<div data-popover-target="card"
|
||||
class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this media:</p>
|
||||
<ul class="list-disc ml-3">
|
||||
<li class="font-normal">{{ result.file.filename|strip_media_path }} — <strong>{{ result.file.size|filesize }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<small
|
||||
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
|
||||
data-action="mouseenter->popover#show mouseleave->popover#hide"
|
||||
>
|
||||
exists
|
||||
</small>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if result.exists == false %}
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white"
|
||||
title="Media has not been downloaded yet.">
|
||||
missing
|
||||
</small>
|
||||
{% endif %}
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
@@ -49,9 +49,32 @@
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<p class="text-gray-50">
|
||||
{{ results.media.description }}
|
||||
</p>
|
||||
|
||||
{% if "movies" == results.media.mediaType %}
|
||||
<div class="flex flex-row justify-start items-end grow">
|
||||
<span class="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 id="movie_results_count">-</span> results
|
||||
</span>
|
||||
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||
{{ results.media.episodeAirDate|date(null, 'UTC') }}
|
||||
</small>
|
||||
|
||||
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||
title: results.media.title,
|
||||
block: 'media_exists_badge',
|
||||
target: "meb_" ~ results.media.imdbId
|
||||
}) }}">
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
||||
missing
|
||||
</small>
|
||||
</twig:Turbo:Frame>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +85,12 @@
|
||||
{{ 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', {
|
||||
tmdbId: results.media.tmdbId,
|
||||
imdbId: results.media.imdbId,
|
||||
target: 'movie_results_frame',
|
||||
block: 'movie_results'
|
||||
}) }}" />
|
||||
</div>
|
||||
{% elseif "tvshows" == results.media.mediaType %}
|
||||
<twig:TvEpisodeList
|
||||
|
||||
19
templates/torrentio/fragments.html.twig
Normal file
19
templates/torrentio/fragments.html.twig
Normal file
@@ -0,0 +1,19 @@
|
||||
{% block movie_results %}
|
||||
<turbo-stream action="replace" targets="#{{ target }}">
|
||||
<template>
|
||||
<div class="p-4 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
<div class="overflow-hidden rounded-md">
|
||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
{% block tvshow_results %}
|
||||
<turbo-stream action="replace" targets="#{{ target }}">
|
||||
<template>
|
||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'tv-results'}) }}
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Hello TorrentioController!{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
|
||||
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
|
||||
</style>
|
||||
|
||||
<div class="example-wrapper">
|
||||
<h1>Hello {{ controller_name }}! ✅</h1>
|
||||
|
||||
This friendly message is coming from:
|
||||
<ul>
|
||||
<li>Your controller at <code>/var/www/src/Controller/TorrentioController.php</code></li>
|
||||
<li>Your template at <code>/var/www/templates/torrentio/index.html.twig</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,13 +1,4 @@
|
||||
<div class="p-4 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
{% if results.file != false %}
|
||||
<div class="p-3 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this movie:</p>
|
||||
<ul class="list-disc ml-3 overflow-scroll">
|
||||
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user