Compare commits

..

7 Commits

40 changed files with 375 additions and 391 deletions

View File

@@ -0,0 +1,30 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
initialize() {}
connect() {}
disconnect() {}
async clearAll() {
let response = await fetch('/api/torrentio/cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
type: 'torrentio',
mediaType: 'tvshows',
})
});
response = await response.json()
}
}

View File

@@ -1,48 +0,0 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static outlets = ['navbar']
initialize() {
// Called once when the controller is first instantiated (per element)
// Here you can initialize variables, create scoped callables for event
// listeners, instantiate external libraries, etc.
// this._fooBar = this.fooBar.bind(this)
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
navbarOutletConnected(outlet) {
console.log(outlet)
}
toggleMenu() {
console.log(this.navbarOutlet);
this.navbarOutlet.toggle();
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@@ -17,15 +17,9 @@ export default class extends Controller {
link.className = this.activeStyles;
}
});
window.addEventListener("resize", (event) => {
});
}
toggle() {
this.element.parentElement.classList.toggle('hidden');
this.element.classList.toggle('fixed');
this.element.classList.toggle('z-20');
setActive() {
}
}

View File

@@ -90,7 +90,13 @@ export default class extends Controller {
}
toggleList() {
this.listTarget.classList.toggle('options-table');
// if (!this.isOpen) {
// this.toggleButtonTarget.classList.add('rotate-180');
// this.toggleButtonTarget.classList.remove('-rotate-180');
// } else {
// this.toggleButtonTarget.classList.add('-rotate-180');
// this.toggleButtonTarget.classList.remove('rotate-180');
// }
this.listTarget.classList.toggle('hidden');
this.toggleButtonTarget.classList.toggle('rotate-90');
this.toggleButtonTarget.classList.toggle('-rotate-90');

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 0 0"><path fill="currentColor" fill-rule="evenodd" d="M0 3.75A.75.75 0 0 1 .75 3h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 3.75M0 8a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8m.75 3.5a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

View File

@@ -25,6 +25,13 @@
.alert-warning {
@apply bg-yellow-500/70 hover:bg-yellow-600 border-yellow-400 text-black
}
.primary-btn {
@apply px-1 py-1 rounded-md bg-orange-500 self-end text-white w-16 ml-2 hover:bg-orange-600
}
.secondary-btn {
@apply px-1 py-1 rounded-md self-end w-16 hover:bg-stone-100
}
}
/* Prevent scrolling while dialog is open */
@@ -63,27 +70,3 @@ dialog[data-dialog-target="dialog"][open] {
dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards;
}
.r-tablecell {
display: none;
}
@media screen and (min-width: 768px) {
.r-tablecell {
display: inline-table;
}
}
.options-table {
display: flex;
:last-child {
border-bottom: none;
}
}
@media screen and (min-width: 768px) {
.options-table {
display: inline-table;
}
}

View File

@@ -15,5 +15,11 @@ framework:
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
pools:
torrentio.cache:
adapter: cache.app
tmdb.cache:
adapter: cache.app
default_lifetime: 2592000
page.cache:
adapter: cache.app

View File

@@ -36,4 +36,5 @@ controllersTorrentio:
namespace: App\Torrentio\Framework\Controller
type: attribute
defaults:
schemes: ['https']
schemes: [ 'https' ]

View File

@@ -40,11 +40,4 @@ return [
'stimulus-use' => [
'version' => '0.52.2',
],
'animate.css' => [
'version' => '4.1.1',
],
'animate.css/animate.min.css' => [
'version' => '4.1.1',
'type' => 'css',
],
];

View File

@@ -5,7 +5,6 @@ namespace App\Controller;
use App\Download\Framework\Repository\DownloadRepository;
use App\Monitor\Action\Command\MonitorTvShowCommand;
use App\Monitor\Action\Handler\MonitorTvShowHandler;
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
use App\Tmdb\Tmdb;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -31,9 +30,9 @@ final class IndexController extends AbstractController
}
#[Route('/test', name: 'app_test')]
public function test(MonitorDispatcher $dispatcher): Response
public function test()
{
$dispatcher();
return new Response();
$result = $this->monitorTvShowHandler->handle(new MonitorTvShowCommand(355));
return $this->json($result);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Torrentio\Framework\Controller;
namespace App\Controller;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
@@ -15,8 +15,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class WebController extends AbstractController
final class TorrentioController extends AbstractController
{
public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
@@ -25,7 +26,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, TagAwareCacheInterface $pageCache): Response
{
$cacheId = sprintf(
"page.torrentio.movies.%s.%s",
@@ -33,17 +34,29 @@ final class WebController extends AbstractController
$input->imdbId
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/movies.html.twig', [
'results' => $results,
]);
});
try {
return $pageCache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['page', 'page.torrentio', 'page.torrentio.movies', "page.torrentio.movies.$input->tmdbId.$input->imdbId", 'torrentio', 'torrentio.movies', "torrentio.movies.$input->tmdbId.$input->imdbId"]);
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/movies.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',
[],
new Response('Too many requests',
Response::HTTP_TOO_MANY_REQUESTS,
['Retry-After' => 4000]
)
);
}
}
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
public function tvShowOptions(GetTvShowOptionsInput $input, TagAwareCacheInterface $pageCache): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
@@ -54,13 +67,14 @@ 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 $pageCache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['page', 'page.torrentio', 'page.torrentio.tvshows', "page.torrentio.tvshows.$input->tmdbId.$input->imdbId", "page.torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season", "page.torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season.$input->episode", 'torrentio', 'torrentio.tvshows', "torrentio.tvshows.$input->tmdbId.$input->imdbId", "torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season", "torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season.$input->episode", $input->imdbId, $input->tmdbId]);
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
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',

View File

@@ -6,10 +6,8 @@ use App\Monitor\Action\Handler\AddMonitorHandler;
use App\Monitor\Action\Handler\DeleteMonitorHandler;
use App\Monitor\Action\Input\AddMonitorInput;
use App\Monitor\Action\Input\DeleteMonitorInput;
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
use App\Util\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Routing\Attribute\Route;
@@ -52,15 +50,4 @@ class ApiController extends AbstractController
'message' => $response
]);
}
#[Route('/api/monitor/dispatch', name: 'api_monitor_dispatch', methods: ['GET'])]
public function dispatch(MonitorDispatcher $dispatcher): Response
{
$dispatcher();
return $this->json([
'status' => 200,
'message' => 'Manually dispatched MonitorDispatcher'
]);
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Monitor\Framework\Entity;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\User\Framework\Entity\User;
use Carbon\Carbon;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -149,7 +148,7 @@ class Monitor
public function getLastSearch(): ?\DateTimeInterface
{
return Carbon::parse($this->lastSearch);
return $this->lastSearch;
}
public function setLastSearch(?\DateTimeInterface $lastSearch): static

View File

@@ -7,7 +7,6 @@ use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
use App\Monitor\Action\Command\MonitorTvSeasonCommand;
use App\Monitor\Action\Command\MonitorTvShowCommand;
use App\Monitor\Framework\Repository\MonitorRepository;
use Carbon\Carbon;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
@@ -24,8 +23,6 @@ class MonitorDispatcher
public function __invoke() {
$this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher');
$this->cleanupStuckMonitors();
$monitorHandlers = [
'movie' => MonitorMovieCommand::class,
'tvepisode' => MonitorTvEpisodeCommand::class,
@@ -44,18 +41,4 @@ class MonitorDispatcher
$this->bus->dispatch(new $command($monitor->getId()));
}
}
private function cleanupStuckMonitors(): void
{
$hoursStuck = 4;
$monitors = $this->monitorRepository->findBy(['status' => 'In Progress']);
foreach ($monitors as $monitor) {
// Reset the status to active so it will be executed again
if ($monitor->getLastSearch()->diffInHours(Carbon::today()) > $hoursStuck) {
$this->logger->info('[MonitorDispatcher] Cleaning up monitor: ' . $monitor->getId() . ' (' . $monitor->getTitle() . '), resetting status to \'Active\' from \''. $monitor->getStatus() .'\' after being stuck for ' . $hoursStuck . ' hours.');
$monitor->setStatus('Active');
}
}
$this->monitorRepository->getEntityManager()->flush();
}
}

View File

@@ -44,7 +44,7 @@ class Tmdb
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
public function __construct(
private readonly CacheItemPoolInterface $cache,
private readonly CacheItemPoolInterface $tmdbCache,
private readonly EventDispatcherInterface $eventDispatcher,
#[Autowire(env: 'TMDB_API')] string $apiKey,
) {
@@ -78,7 +78,7 @@ class Tmdb
$requestListener = new Psr6CachedRequestListener(
$this->client->getHttpClient(),
$this->eventDispatcher,
$cache,
$tmdbCache,
$this->client->getHttpClient()->getPsr17StreamFactory(),
[]
);
@@ -325,7 +325,7 @@ class Tmdb
public function getImdbId(string $tmdbId, $mediaType)
{
$externalIds = $this->cache->get("tmdb.externalIds.{$tmdbId}",
$externalIds = $this->tmdbCache->get("tmdb.externalIds.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:
@@ -346,7 +346,7 @@ class Tmdb
public function getImages($tmdbId, $mediaType)
{
return $this->cache->get("tmdb.images.{$tmdbId}",
return $this->tmdbCache->get("tmdb.images.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value:

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Torrentio\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class DeleteCacheCommand implements CommandInterface
{
public function __construct(
public ?string $type = null,
public ?string $mediaType = null,
public ?string $tmdbId = null,
public ?string $imdbId = null,
public ?int $season = null,
public ?int $episode = null,
public ?array $tags = null,
) {}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Torrentio\Action\Handler;
use Aimeos\Map;
use App\Torrentio\Action\Command\DeleteCacheCommand;
use App\Torrentio\Action\Result\DeleteCacheResult;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/** @implements HandlerInterface<DeleteCacheCommand, DeleteCacheResult> */
class DeleteCacheHandler implements HandlerInterface
{
public function __construct(
private readonly TagAwareCacheInterface $torrentioCache
) {}
public function handle(CommandInterface $command): ResultInterface
{
$input = Map::from((array) $command)
->filter(fn ($value, $key) => null !== $value && "" !== $value)
;
$cacheKey = null;
if ($input->has('type')) {
$cacheKey = $input->get('type');
if ($input->has('mediaType')) {
$cacheKey .= ".".$input->get('mediaType');
if ($input->has('tmdbId')) {
$cacheKey .= ".".$input->get('tmdbId');
if ($input->has('imdbId')) {
$cacheKey .= ".".$input->get('imdbId');
if ($input->has('season')) {
$cacheKey .= ".".$input->get('season');
if ($input->has('episode')) {
$cacheKey .= ".".$input->get('episode');
}
}
}
}
}
}
if ($cacheKey !== null) {
$this->torrentioCache->invalidateTags([$cacheKey]);
}
if ($input->has('tags')) {
$this->torrentioCache->invalidateTags($input->get('tags'));
}
return new DeleteCacheResult($input, $cacheKey, $command->tags);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Torrentio\Action\Input;
use App\Torrentio\Action\Command\DeleteCacheCommand;
use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/**
* @implements DeleteCacheInput<DeleteCacheCommand>
*/
class DeleteCacheInput implements InputInterface
{
public function __construct(
#[SourceRequest('type', nullify: true)]
public ?string $type,
#[SourceRequest('mediaType', nullify: true)]
public ?string $mediaType,
#[SourceRequest('tmdbId', nullify: true)]
public ?string $tmdbId,
#[SourceRequest('imdbId', nullify: true)]
public ?string $imdbId,
#[SourceRequest('season', nullify: true)]
public ?int $season,
#[SourceRequest('episode', nullify: true)]
public ?int $episode,
#[SourceRequest('tags', nullify: true)]
public ?array $tags,
) {}
public function toCommand(): CommandInterface
{
return new DeleteCacheCommand(
type: $this->type,
mediaType: $this->mediaType,
tmdbId: $this->tmdbId,
imdbId: $this->imdbId,
season: $this->season,
episode: $this->episode
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Torrentio\Action\Result;
use Aimeos\Map;
use OneToMany\RichBundle\Contract\ResultInterface;
class DeleteCacheResult implements ResultInterface
{
public function __construct(
public Map $result,
public ?string $cacheKey = null,
public ?array $tags = null,
) {}
}

View File

@@ -23,7 +23,7 @@ class Torrentio
public function __construct(
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
private TagAwareCacheInterface $cache,
private TagAwareCacheInterface $torrentioCache,
private LoggerInterface $logger,
) {
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
@@ -32,11 +32,11 @@ class Torrentio
]);
}
public function search(string $imdbCode, string $type, bool $parseResults = true): array
public function search(string $imdbCode, string $type, array $filter = []): array
{
$cacheKey = "torrentio.{$imdbCode}";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
$results = $this->torrentioCache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['torrentio', $type, $imdbCode]);
try {
@@ -56,17 +56,13 @@ class Torrentio
return [];
});
if (true === $parseResults) {
return $this->parse($results);
}
return $results;
return $this->parse($results, $filter);
}
public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): array
public function fetchEpisodeResults(string $imdbId, int $season, int $episode): array
{
$cacheKey = "torrentio.$imdbId.$season.$episode";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
$results = $this->torrentioCache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
try {
@@ -90,15 +86,18 @@ class Torrentio
throw new TorrentioRateLimitException();
}
if (true === $parseResults) {
return $this->parse($results);
}
return $results;
return $this->parse($results, []);
}
public function parse(array $data): array
public function parse(array $data, array $filter): array
{
$ruleEngine = new RuleEngine();
foreach ($filter as $rule => $value) {
if ('resolution' === $rule) {
$ruleEngine->addRule(new Resolution($value));
}
}
$results = [];
foreach ($data['streams'] as $stream) {
if (!str_starts_with($stream['url'], "https")) {
@@ -120,7 +119,9 @@ class Torrentio
$bingeGroup
);
$results[] = $result;
if ($ruleEngine->validateAll($result)) {
$results[] = $result;
}
}
return $results;

View File

@@ -2,115 +2,20 @@
namespace App\Torrentio\Framework\Controller;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
use App\Torrentio\Action\Input\GetMovieOptionsInput;
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
use App\Torrentio\Client\Torrentio;
use App\Torrentio\Exception\TorrentioRateLimitException;
use App\Util\Broadcaster;
use Carbon\Carbon;
use App\Torrentio\Action\Handler\DeleteCacheHandler;
use App\Torrentio\Action\Input\DeleteCacheInput;
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;
final class ApiController extends AbstractController
class ApiController extends AbstractController
{
public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
private readonly Broadcaster $broadcaster,
private readonly Torrentio $torrentio,
) {}
#[Route('/api/torrentio/{imdbId}/{season?}/{episode?}', name: 'api_torrentio')]
public function api(string $imdbId, ?int $season, ?int $episode): Response
{
if (null !== $season && null !== $episode) {
return $this->json(
$this->torrentio->fetchEpisodeResults($imdbId, $season, $episode, false)
);
}
return $this->json(
$this->torrentio->search($imdbId, 'movies', false),
);
}
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
{
$cacheId = sprintf(
"page.torrentio.movies.%s.%s",
$input->tmdbId,
$input->imdbId
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/movies.html.twig', [
'results' => $results,
]);
});
}
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
$input->tmdbId,
$input->imdbId,
$input->season,
$input->episode,
);
try {
// 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,
]);
// });
} 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',
[],
new Response('Too many requests',
Response::HTTP_TOO_MANY_REQUESTS,
['Retry-After' => 4000]
)
);
}
}
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
$input->tmdbId,
$input->imdbId,
$input->season,
$input->episode,
);
$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,
]);
});
#[Route('/api/torrentio/cache', name: 'api.torrentio.cache', methods: ['POST'])]
public function deleteCache(
DeleteCacheInput $deleteCacheInput,
DeleteCacheHandler $deleteCacheHandler,
): Response {
$result = $deleteCacheHandler->handle($deleteCacheInput->toCommand());
return $this->json($result, Response::HTTP_OK);
}
}

View File

@@ -2,7 +2,6 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}

View File

@@ -2,7 +2,6 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
@@ -14,13 +13,18 @@
{% endblock %}
</head>
<body class="flex flex-col bg-stone-700">
<div class="grid md:grid-cols-6">
<div class="hidden md:block md:col-span-1 md:h-screen">
<div class="grid grid-cols-6">
<div class="col-span-1 h-screen">
<twig:NavBar />
</div>
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
<div class="col-span-5 h-screen overflow-y-scroll">
<twig:Header />
<h2 class="px-4 my-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
<div class="px-4 mt-3 flex flex-row justify-between">
<h2 class="m-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
<div class="flex flex-row gap-1 align-end justify-end items-end">
{% block action_buttons %}{% endblock %}
</div>
</div>
{% block body %}{% endblock %}
</div>
</div>

View File

@@ -11,17 +11,21 @@
<thead>
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate">
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
Filename
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
Media type
</th>
{% if this.isWidget == false %}
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
Filename
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
Media type
</th>
{% endif %}
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
Progress
@@ -34,7 +38,7 @@
<tbody id="{{ table_body_id }}" class="divide-y divide-gray-200 dark:divide-gray-50">
{% if this.downloads.items|length > 0 %}
{% for download in this.downloads.items %}
<twig:DownloadListRow download="{{ download }}" isWidget="{{ isWidget }}" />
<twig:DownloadListRow download="{{ download }}" isWidget="{{ this.isWidget }}" />
{% endfor %}
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
<tr id="download_view_all">

View File

@@ -1,7 +1,7 @@
<tr{{ attributes }} class="hover:bg-gray-200" id="ad_download_{{ download.id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate"
class="mr-1 hover:underline rounded-md"
>
{{ download.title }}
</a>
@@ -11,13 +11,14 @@
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] {{ isWidget == true ? "hidden" : "r-tablecell" }} truncate">
{{ download.filename }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
{{ download.mediaType }}
</td>
{% if isWidget == false %}
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] truncate">
{{ download.filename }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
{{ download.mediaType }}
</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
{% if download.progress < 100 %}
@@ -31,7 +32,7 @@
<twig:StatusBadge color="green" status="Complete" />
{% endif %}
</td>
<td id="hidden md:table-cell action_buttons_{{ download.id }}" class="px-6 py-4 flex flex-row items-center">
<td id="action_buttons_{{ download.id }}" class="px-6 py-4 flex flex-row items-center">
{% if download.status == 'In Progress' and download.progress < 100 %}
<button id="pause_{{ download.id }}" class="text-orange-500 hover:text-orange-600 mr-1 self-start" {{ stimulus_action('download_list', 'pauseDownload', 'click', {id: download.id}) }}>
<twig:ux:icon name="icon-park-twotone:pause-one" width="16.75px" height="16.75px" class="rounded-full" />

View File

@@ -6,7 +6,7 @@
data-result-filter-tv-results-outlet=".results"
data-result-filter-tv-episode-list-outlet=".episode-list"
>
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
<div class="w-full p-4 flex flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
<label for="resolution">
Resolution
<select id="resolution"

View File

@@ -5,21 +5,12 @@
<div class="md:flex md:items-center md:gap-12">
<nav aria-label="Global" class="md:block">
<ul class="flex items-center gap-6 text-sm">
<li class="hidden">
<twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/>
</li>
<li class="hidden md:block">
<li><twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/></li>
<li>
<a href="{{ path('app_logout') }}">
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
</a>
</li>
<li {{ stimulus_controller('hamburger') }}
{{ stimulus_action('hamburger', 'toggleMenu', 'click') }}
data-hamburger-navbar-outlet="#navbar"
id="hamburger" class="cursor-pointer md:hidden"
>
<svg xmlns="http://www.w3.org/2000/svg" class="text-orange-500 ml-4" width="25px" height="25px" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M0 3.75A.75.75 0 0 1 .75 3h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 3.75M0 8a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8m.75 3.5a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z" clip-rule="evenodd"/></svg>
</li>
</ul>
</nav>
</div>

View File

@@ -9,12 +9,12 @@
{% if show_cancel is defined or show_submit is defined %}
<div class="flex justify-end">
{% if show_cancel is defined %}
<button type="button" data-action="dialog#close" class="px-1 py-1 rounded-md self-end w-16 hover:bg-stone-100" autofocus>
<button type="button" data-action="dialog#close" class="secondary-btn" autofocus>
{{ cancel_text|default('Cancel') }}
</button>
{% endif %}
{% if show_submit is defined %}
<button type="button" {{ submit_action|raw }} class="px-1 py-1 rounded-md bg-orange-500 self-end text-white w-16 ml-2 hover:bg-orange-600" autofocus>
<button type="button" {{ submit_action|raw }} class="primary-btn" autofocus>
{{ submit_text|default('Submit') }}
</button>
{% endif %}
@@ -22,5 +22,5 @@
{% endif %}
</dialog>
<button type="button" data-action="dialog#open">{{ button_text|raw }}</button>
<button type="button" data-action="dialog#open" class="{{ button_class|default('') }}">{{ button_text|raw }}</button>
</div>

View File

@@ -16,32 +16,32 @@
ID
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start text-xs font-medium uppercase">
Search Count
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start text-xs font-medium uppercase">
Created at
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start text-xs font-medium uppercase">
Last Search Date
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start text-xs font-medium uppercase">
Type
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase">
Status
</th>
<th class="hidden md:table-cell"></th>
<th></th>
</tr>
</thead>
<tbody id="monitors" class="divide-y divide-gray-50">
{% if this.monitors.items|length > 0 %}
{% for monitor in this.monitors.items %}
<twig:MonitorListRow :monitor="monitor" isWidget="{{ this.isWidget }}" />
<twig:MonitorListRow :monitor="monitor" />
{% endfor %}
{% if this.isWidget and this.monitors.items|length > 5 %}
<tr id="monitor_view_all">

View File

@@ -9,16 +9,16 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor|monitor_media_id }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.searchCount }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %}
@@ -36,7 +36,7 @@
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
{% endif %}
</td>
<td class="hidden md:table-cell px-6 py-4 flex flex-row align-middle justify-center">
<td class="px-6 py-4 flex flex-row align-middle justify-center">
{% set delete_button = component('ux:icon', {name: 'ic:twotone-cancel', width: '18px', class: 'rounded-full align-middle text-red-600' }) %}
<twig:Modal heading="But wait!" button_text="{{ delete_button }}" submit_action="{{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: monitor.id}) }}" show_cancel show_submit>
Are you sure you want to delete this monitor?<br />

View File

@@ -1,4 +1,4 @@
<nav id="navbar" {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950 animate__animated animate__slideInLeft animate__slow">
<nav {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950">
<div class="px-4 py-4 flex flex-col gap-12">
<h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1>
<ul class="space-y-1">

View File

@@ -3,12 +3,12 @@
mediaType: mediaType,
imdbId: imdbId
}) }}">
<img src="{{ image }}" class="w-full md:w-40 rounded-md" />
<img src="{{ image }}" class="w-40 rounded-md" />
</a>
<a href="{{ path('app_search_result', {
mediaType: mediaType,
imdbId: imdbId
}) }}">
<h3 class="text-center text-white text-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
<h3 class="text-center text-gray-50 max-w-[16ch] text-extrabold">{{ title }}</h3>
</a>
</div>

View File

@@ -1,9 +1,9 @@
<div{{ attributes }}>
<div class="p-4 flex flex-col md:flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
<div class="p-4 flex flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
{% if poster != null and poster != "https://image.tmdb.org/t/p/w500" %}
<img class="w-full md:w-24 rounded-lg" src="{{ poster }}" />
<img class="w-24 rounded-lg" src="{{ poster }}" />
{% else %}
<div class="w-full md:w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<div class="w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<twig:ux:icon width="16" name="hugeicons:loading-01" />
</div>
{% endif %}
@@ -12,11 +12,11 @@
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
{{ title }} - {{ year }}
</h3>
<p class="hidden md:block md:text-gray-50">
<p class="text-gray-50">
{{ description }}
</p>
</div>
<a class="h-9 rounded-md py-1 px-2 bg-green-600 text-gray-50 text-center"
<a class="h-9 rounded-md py-1 px-2 bg-green-600 text-gray-50"
href="{{ path('app_search_result', {mediaType: mediaType, imdbId: imdbId}) }}"
>choose</a>
</div>

View File

@@ -15,12 +15,12 @@
active: 'true',
}) }}
>
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
<div class="flex flex-col md:flex-row gap-4">
<div class="p-6 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
<div class="flex flex-row gap-4">
{% if episode['poster'] != null %}
<img class="w-full md:w-64 rounded-lg" src="{{ episode['poster'] }}" />
<img class="w-64 rounded-lg" src="{{ episode['poster'] }}" />
{% else %}
<div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<div class="w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<twig:ux:icon width="32" name="hugeicons:loading-01" />
</div>
{% endif %}

View File

@@ -9,7 +9,6 @@
<twig:DownloadList type="active" :isWidget="false" :perPage="10"></twig:DownloadList>
</twig:Card>
</div>
<div class="p-4">
<twig:Card title="Recent Downloads">
<twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList>

View File

@@ -1,11 +1,11 @@
{% extends 'base.html.twig' %}
{% block title %}Dashboard &mdash; Torsearch{% endblock %}
{% block h2 %}Dashboard{% endblock %}
{% block body %}
<div class="p-4 flex flex-col grow gap-4 z-30">
<h2 class="mb-2 text-3xl font-bold text-gray-50">Dashboard</h2>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-row gap-4">
<twig:Card title="Active Downloads" class="w-full">
<twig:DownloadList :type="'active'" />
</twig:Card>
@@ -14,13 +14,13 @@
<twig:DownloadList :type="'complete'" />
</twig:Card>
</div>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-row gap-4">
<twig:Card title="Monitors" class="w-full">
<twig:MonitorList :type="'active'" :isWidget="true" />
</twig:Card>
</div>
<div class="flex flex-col gap-4">
<twig:Card title="Popular Movies" contentClass="flex flex-col gap-4 md:flex-row md:justify-between w-full">
<twig:Card title="Popular Movies" contentClass="flex flex-row justify-between w-full">
{% for movie in popular_movies %}
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"
@@ -32,7 +32,7 @@
/>
{% endfor %}
</twig:Card>
<twig:Card title="Popular TV Shows" contentClass="flex flex-col md:flex-row justify-between w-full">
<twig:Card title="Popular TV Shows" contentClass="flex flex-row justify-between w-full">
{% for movie in popular_tvshows %}
<twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}"

View File

@@ -4,16 +4,14 @@
{% block h2 %}Monitors{% endblock %}
{% block body %}
<div class="flex flex-row">
<div class="p-2 flex flex-col gap-4">
<twig:Card title="Active Monitors">
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
<twig:Card title="Complete Monitors">
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
<div class="p-4">
<twig:Card title="Active Monitors" class="w-full">
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
<div class="p-4">
<twig:Card title="Complete Monitors" class="w-full">
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card>
</div>
{% endblock %}

View File

@@ -7,11 +7,11 @@
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
<div class="flex flex-row w-full gap-2">
<twig:Card title="" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
<div class="p-4 flex flex-row gap-6">
{% if results.media.poster != null %}
<img class="w-full md:w-40 rounded-lg" src="{{ results.media.poster }}" />
<img class="w-40 rounded-lg" src="{{ results.media.poster }}" />
{% else %}
<div class="w-full md:w-40 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<div class="w-40 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
<twig:ux:icon width="24" name="hugeicons:loading-01" />
</div>
{% endif %}
@@ -127,20 +127,4 @@
</twig:Card>
</div>
</div>
<style>
html,
body {
height: 100%;
}
@media (min-width: 640px) {
thead tr:not(:first-child) {
display: none;
}
}
td:not(:last-child) {
border-bottom: 0;
}
</style>
{% endblock %}

View File

@@ -1,69 +1,66 @@
<table class="w-full max-w-[75vw] text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 flex-row flex-no-wrap {{ results.media.mediaType == "tvshows" ? "hidden" : "options-table" }}"
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 {{ results.media.mediaType == "tvshows" ? "hidden" }}"
{{ stimulus_target(controller, "list") }}
>
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
{% for result in results.results %}
<tr class="dark:bg-stone-600 overflow-hidden flex flex-col md:flex-col flex-no wrap md:table-row border-b border-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="dark:bg-stone-600 overflow-hidden">
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Size
</th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Resolution
</th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Codec
</th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Seeders
</th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Provider
</th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Language
</th>
<th scope="col"
class="px-4 py-4 leading-[32px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
Actions
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
</th>
</tr>
{% endfor %}
</thead>
<tbody class="flex-1 sm:flex-none">
<tbody>
{% for result in results.results %}
<tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap sm:table-row border-b border-gray-500" data-provider="{{ result.provider }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
<td id="size" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
<tr class="bg-white border-b dark:bg-slate-700 dark:border-gray-600 border-gray-200" data-provider="{{ result.provider }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
<td id="size" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.size }}
</td>
<td id="resolution" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
<td id="resolution" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.resolution }}
</td>
<td id="codec" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
<td id="codec" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.codec }}
</td>
<td id="seeders" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
<td id="seeders" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.seeders }}
</td>
<td id="provider" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50 " data-provider="{{ result.provider }}">
<td id="provider" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50" data-provider="{{ result.provider }}">
{{ result.provider }}
</td>
<td id="language" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50 overflow-scroll" data-languages="{{ result.languages|json_encode }}">
<td id="language" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50" data-languages="{{ result.languages|json_encode }}">
{{ result.languageFlags|raw }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-end">
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50"
{{ stimulus_controller('download_button', {
url: result.url,
title: results.media.title,
filename: result.filename,
mediaType: results.media.mediaType,
imdbId: results.media.imdbId ?? app.current_route_parameters.imdbId,
imdbId: results.media.imdbId,
episodeId: results|episode_id_from_results
}) }}
{{ stimulus_action('download_button', 'download', 'click') }}

View File

@@ -2,8 +2,20 @@
{% block title %}Preferences{% endblock %}
{% block h2 %}Preferences{% endblock %}
{% block action_buttons %}
<div {{ stimulus_controller('clear_cache') }}>
<twig:Modal heading="Hold on a sec!" button_text="Clear Cache" cancel_text="Nope" submit_text="Yep" show_cancel show_submit
button_class="px-1.5 py-1 my-2 text-white text-sm bg-blue-950 hover:bg-black/80 border-2 border-blue-500/90 rounded-md inline-block"
submit_action="{{ stimulus_action('clear_cache', 'clearAll', 'click') }}"
>
This will clear the TMDB, Torrentio, and application cache. Clearing the cache is safe, but may lead to
slower page loads and rate limits by Torrentio. Would you like to proceed?
</twig:Modal>
</div>
{% endblock %}
{% block body %}
<div class="p-4 flex flex-col md:flex-row gap-2">
<div class="p-4 flex flex-row gap-2">
<twig:Card title="Media Preferences" class="w-full">
<p class="text-gray-50 mb-2">Define a filter to be pre-applied to your download options.</p>
<form id="media_preferences" class="flex flex-col max-w-64" name="media_preferences" method="post" action="{{ path('app_save_media_preferences') }}">