Compare commits

..

7 Commits

42 changed files with 376 additions and 405 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

@@ -56,7 +56,6 @@ export default class extends Controller {
} }
let include = true; let include = true;
option.classList.add('r-tablerow');
option.classList.remove('hidden'); option.classList.remove('hidden');
option.querySelector('input[type="checkbox"]').checked = false; option.querySelector('input[type="checkbox"]').checked = false;
@@ -82,7 +81,6 @@ export default class extends Controller {
} }
if (false === include) { if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden'); option.classList.add('hidden');
} else if (true === firstIncluded) { } else if (true === firstIncluded) {
count = 1; count = 1;

View File

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

View File

@@ -90,7 +90,13 @@ export default class extends Controller {
} }
toggleList() { 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.listTarget.classList.toggle('hidden');
this.toggleButtonTarget.classList.toggle('rotate-90'); this.toggleButtonTarget.classList.toggle('rotate-90');
this.toggleButtonTarget.classList.toggle('-rotate-90'); this.toggleButtonTarget.classList.toggle('-rotate-90');
@@ -135,7 +141,6 @@ export default class extends Controller {
} }
let include = true; let include = true;
option.classList.add('r-tablerow');
option.classList.remove('hidden'); option.classList.remove('hidden');
option.querySelector('input[type="checkbox"]').checked = false; option.querySelector('input[type="checkbox"]').checked = false;
@@ -161,7 +166,6 @@ export default class extends Controller {
} }
if (false === include) { if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden'); option.classList.add('hidden');
} else if (true === firstIncluded) { } else if (true === firstIncluded) {
count = 1; count = 1;

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 { .alert-warning {
@apply bg-yellow-500/70 hover:bg-yellow-600 border-yellow-400 text-black @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 */ /* Prevent scrolling while dialog is open */
@@ -63,35 +70,3 @@ dialog[data-dialog-target="dialog"][open] {
dialog[data-dialog-target="dialog"][closing] { dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards; animation: fade-out 200ms forwards;
} }
.r-tablecell {
display: none;
}
.r-tablerow {
display: flex;
}
@media screen and (min-width: 768px) {
.r-tablecell {
display: inline-table;
}
.r-tablerow {
display: table-row;
}
}
.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 #app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default # Namespaced pools use the above "app" backend by default
#pools: pools:
#my.dedicated.cache: null 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 namespace: App\Torrentio\Framework\Controller
type: attribute type: attribute
defaults: defaults:
schemes: ['https'] schemes: [ 'https' ]

View File

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

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Torrentio\Framework\Controller; namespace App\Controller;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler; use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler; use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
@@ -15,8 +15,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class WebController extends AbstractController final class TorrentioController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler, private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
@@ -25,7 +26,7 @@ final class WebController extends AbstractController
) {} ) {}
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')] #[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( $cacheId = sprintf(
"page.torrentio.movies.%s.%s", "page.torrentio.movies.%s.%s",
@@ -33,17 +34,29 @@ final class WebController extends AbstractController
$input->imdbId $input->imdbId
); );
return $cache->get($cacheId, function (ItemInterface $item) use ($input) { try {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); return $pageCache->get($cacheId, function (ItemInterface $item) use ($input) {
$results = $this->getMovieOptionsHandler->handle($input->toCommand()); $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
return $this->render('torrentio/movies.html.twig', [ $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' => $results, $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')] #[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( $cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s", "page.torrentio.tvshows.%s.%s.%s.%s",
@@ -54,13 +67,14 @@ final class WebController extends AbstractController
); );
try { try {
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) { return $pageCache->get($cacheId, function (ItemInterface $item) use ($input) {
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); $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()); $results = $this->getTvShowOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/tvshows.html.twig', [ return $this->render('torrentio/tvshows.html.twig', [
'results' => $results, 'results' => $results,
]); ]);
// }); });
} catch (TorrentioRateLimitException $exception) { } catch (TorrentioRateLimitException $exception) {
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning'); $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', 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\Handler\DeleteMonitorHandler;
use App\Monitor\Action\Input\AddMonitorInput; use App\Monitor\Action\Input\AddMonitorInput;
use App\Monitor\Action\Input\DeleteMonitorInput; use App\Monitor\Action\Input\DeleteMonitorInput;
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
use App\Util\Broadcaster; use App\Util\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -52,15 +50,4 @@ class ApiController extends AbstractController
'message' => $response '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\Monitor\Framework\Repository\MonitorRepository;
use App\User\Framework\Entity\User; use App\User\Framework\Entity\User;
use Carbon\Carbon;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -149,7 +148,7 @@ class Monitor
public function getLastSearch(): ?\DateTimeInterface public function getLastSearch(): ?\DateTimeInterface
{ {
return Carbon::parse($this->lastSearch); return $this->lastSearch;
} }
public function setLastSearch(?\DateTimeInterface $lastSearch): static 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\MonitorTvSeasonCommand;
use App\Monitor\Action\Command\MonitorTvShowCommand; use App\Monitor\Action\Command\MonitorTvShowCommand;
use App\Monitor\Framework\Repository\MonitorRepository; use App\Monitor\Framework\Repository\MonitorRepository;
use Carbon\Carbon;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask; use Symfony\Component\Scheduler\Attribute\AsCronTask;
@@ -24,8 +23,6 @@ class MonitorDispatcher
public function __invoke() { public function __invoke() {
$this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher'); $this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher');
$this->cleanupStuckMonitors();
$monitorHandlers = [ $monitorHandlers = [
'movie' => MonitorMovieCommand::class, 'movie' => MonitorMovieCommand::class,
'tvepisode' => MonitorTvEpisodeCommand::class, 'tvepisode' => MonitorTvEpisodeCommand::class,
@@ -44,18 +41,4 @@ class MonitorDispatcher
$this->bus->dispatch(new $command($monitor->getId())); $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"; const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
public function __construct( public function __construct(
private readonly CacheItemPoolInterface $cache, private readonly CacheItemPoolInterface $tmdbCache,
private readonly EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
#[Autowire(env: 'TMDB_API')] string $apiKey, #[Autowire(env: 'TMDB_API')] string $apiKey,
) { ) {
@@ -78,7 +78,7 @@ class Tmdb
$requestListener = new Psr6CachedRequestListener( $requestListener = new Psr6CachedRequestListener(
$this->client->getHttpClient(), $this->client->getHttpClient(),
$this->eventDispatcher, $this->eventDispatcher,
$cache, $tmdbCache,
$this->client->getHttpClient()->getPsr17StreamFactory(), $this->client->getHttpClient()->getPsr17StreamFactory(),
[] []
); );
@@ -325,7 +325,7 @@ class Tmdb
public function getImdbId(string $tmdbId, $mediaType) 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) { function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) { switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value: case MediaType::Movie->value:
@@ -346,7 +346,7 @@ class Tmdb
public function getImages($tmdbId, $mediaType) 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) { function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) { switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->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( public function __construct(
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey, #[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
private TagAwareCacheInterface $cache, private TagAwareCacheInterface $torrentioCache,
private LoggerInterface $logger, private LoggerInterface $logger,
) { ) {
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl); $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}"; $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->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['torrentio', $type, $imdbCode]); $item->tag(['torrentio', $type, $imdbCode]);
try { try {
@@ -56,17 +56,13 @@ class Torrentio
return []; return [];
}); });
if (true === $parseResults) { return $this->parse($results, $filter);
return $this->parse($results);
}
return $results;
} }
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"; $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->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"]); $item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
try { try {
@@ -90,15 +86,18 @@ class Torrentio
throw new TorrentioRateLimitException(); throw new TorrentioRateLimitException();
} }
if (true === $parseResults) { return $this->parse($results, []);
return $this->parse($results);
}
return $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 = []; $results = [];
foreach ($data['streams'] as $stream) { foreach ($data['streams'] as $stream) {
if (!str_starts_with($stream['url'], "https")) { if (!str_starts_with($stream['url'], "https")) {
@@ -120,7 +119,9 @@ class Torrentio
$bingeGroup $bingeGroup
); );
$results[] = $result; if ($ruleEngine->validateAll($result)) {
$results[] = $result;
}
} }
return $results; return $results;

View File

@@ -2,115 +2,20 @@
namespace App\Torrentio\Framework\Controller; namespace App\Torrentio\Framework\Controller;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler; use App\Torrentio\Action\Handler\DeleteCacheHandler;
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler; use App\Torrentio\Action\Input\DeleteCacheInput;
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 Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; 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( #[Route('/api/torrentio/cache', name: 'api.torrentio.cache', methods: ['POST'])]
private readonly GetMovieOptionsHandler $getMovieOptionsHandler, public function deleteCache(
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler, DeleteCacheInput $deleteCacheInput,
private readonly Broadcaster $broadcaster, DeleteCacheHandler $deleteCacheHandler,
private readonly Torrentio $torrentio, ): Response {
) {} $result = $deleteCacheHandler->handle($deleteCacheInput->toCommand());
return $this->json($result, Response::HTTP_OK);
#[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,
]);
});
} }
} }

View File

@@ -21,8 +21,7 @@ module.exports = {
"transition-opacity", "transition-opacity",
"ease-in", "ease-in",
"duration-700", "duration-700",
"opacity-100", "opacity-100"
"table-row"
], ],
theme: { theme: {
extend: { extend: {

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Welcome!{% endblock %}</title> <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>"> <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 %} {% block stylesheets %}

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Welcome!{% endblock %}</title> <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>"> <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 %} {% block stylesheets %}
@@ -14,13 +13,18 @@
{% endblock %} {% endblock %}
</head> </head>
<body class="flex flex-col bg-stone-700"> <body class="flex flex-col bg-stone-700">
<div class="grid md:grid-cols-6"> <div class="grid grid-cols-6">
<div class="hidden md:block md:col-span-1 md:h-screen"> <div class="col-span-1 h-screen">
<twig:NavBar /> <twig:NavBar />
</div> </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 /> <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 %} {% block body %}{% endblock %}
</div> </div>
</div> </div>

View File

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

View File

@@ -1,7 +1,7 @@
<tr{{ attributes }} class="hover:bg-gray-200" id="ad_download_{{ download.id }}"> <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"> <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}) }}" <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 }} {{ download.title }}
</a> </a>
@@ -11,13 +11,14 @@
{% endif %} {% endif %}
</td> </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"> {% if isWidget == false %}
{{ download.filename }} <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] truncate">
</td> {{ 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" }}"> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
{{ download.mediaType }} {{ download.mediaType }}
</td> </td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50"> <td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
{% if download.progress < 100 %} {% if download.progress < 100 %}
@@ -31,7 +32,7 @@
<twig:StatusBadge color="green" status="Complete" /> <twig:StatusBadge color="green" status="Complete" />
{% endif %} {% endif %}
</td> </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 %} {% 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}) }}> <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" /> <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-results-outlet=".results"
data-result-filter-tv-episode-list-outlet=".episode-list" 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"> <label for="resolution">
Resolution Resolution
<select id="resolution" <select id="resolution"

View File

@@ -5,21 +5,12 @@
<div class="md:flex md:items-center md:gap-12"> <div class="md:flex md:items-center md:gap-12">
<nav aria-label="Global" class="md:block"> <nav aria-label="Global" class="md:block">
<ul class="flex items-center gap-6 text-sm"> <ul class="flex items-center gap-6 text-sm">
<li class="hidden"> <li><twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/></li>
<twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/> <li>
</li>
<li class="hidden md:block">
<a href="{{ path('app_logout') }}"> <a href="{{ path('app_logout') }}">
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" /> <twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
</a> </a>
</li> </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> </ul>
</nav> </nav>
</div> </div>

View File

@@ -9,12 +9,12 @@
{% if show_cancel is defined or show_submit is defined %} {% if show_cancel is defined or show_submit is defined %}
<div class="flex justify-end"> <div class="flex justify-end">
{% if show_cancel is defined %} {% 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') }} {{ cancel_text|default('Cancel') }}
</button> </button>
{% endif %} {% endif %}
{% if show_submit is defined %} {% 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') }} {{ submit_text|default('Submit') }}
</button> </button>
{% endif %} {% endif %}
@@ -22,5 +22,5 @@
{% endif %} {% endif %}
</dialog> </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> </div>

View File

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

View File

@@ -9,16 +9,16 @@
<td class="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|monitor_media_id }} {{ monitor|monitor_media_id }}
</td> </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 }} {{ monitor.searchCount }}
</td> </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') }} {{ monitor.createdAt|date('m/d/Y h:i a') }}
</td> </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') }} {{ monitor.lastSearch|date('m/d/Y h:i a') }}
</td> </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" %} {% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" /> <twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %} {% elseif monitor.monitorType == "tvseason" %}
@@ -36,7 +36,7 @@
<twig:StatusBadge color="green" status="{{ monitor.status }}" /> <twig:StatusBadge color="green" status="{{ monitor.status }}" />
{% endif %} {% endif %}
</td> </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' }) %} {% 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> <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 /> 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"> <div class="px-4 py-4 flex flex-col gap-12">
<h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1> <h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1>
<ul class="space-y-1"> <ul class="space-y-1">

View File

@@ -3,12 +3,12 @@
mediaType: mediaType, mediaType: mediaType,
imdbId: imdbId imdbId: imdbId
}) }}"> }) }}">
<img src="{{ image }}" class="w-full md:w-40 rounded-md" /> <img src="{{ image }}" class="w-40 rounded-md" />
</a> </a>
<a href="{{ path('app_search_result', { <a href="{{ path('app_search_result', {
mediaType: mediaType, mediaType: mediaType,
imdbId: imdbId 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> </a>
</div> </div>

View File

@@ -1,9 +1,9 @@
<div{{ attributes }}> <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" %} {% 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 %} {% 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" /> <twig:ux:icon width="16" name="hugeicons:loading-01" />
</div> </div>
{% endif %} {% endif %}
@@ -12,11 +12,11 @@
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50"> <h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
{{ title }} - {{ year }} {{ title }} - {{ year }}
</h3> </h3>
<p class="hidden md:block md:text-gray-50"> <p class="text-gray-50">
{{ description }} {{ description }}
</p> </p>
</div> </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}) }}" href="{{ path('app_search_result', {mediaType: mediaType, imdbId: imdbId}) }}"
>choose</a> >choose</a>
</div> </div>

View File

@@ -15,12 +15,12 @@
active: 'true', 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="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="flex flex-row gap-4">
{% if episode['poster'] != null %} {% 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 %} {% 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" /> <twig:ux:icon width="32" name="hugeicons:loading-01" />
</div> </div>
{% endif %} {% endif %}

View File

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

View File

@@ -1,11 +1,11 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Dashboard &mdash; Torsearch{% endblock %} {% block title %}Dashboard &mdash; Torsearch{% endblock %}
{% block h2 %}Dashboard{% endblock %}
{% block body %} {% block body %}
<div class="p-4 flex flex-col grow gap-4 z-30"> <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-row gap-4">
<div class="flex flex-col md:flex-row gap-4">
<twig:Card title="Active Downloads" class="w-full"> <twig:Card title="Active Downloads" class="w-full">
<twig:DownloadList :type="'active'" /> <twig:DownloadList :type="'active'" />
</twig:Card> </twig:Card>
@@ -14,13 +14,13 @@
<twig:DownloadList :type="'complete'" /> <twig:DownloadList :type="'complete'" />
</twig:Card> </twig:Card>
</div> </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:Card title="Monitors" class="w-full">
<twig:MonitorList :type="'active'" :isWidget="true" /> <twig:MonitorList :type="'active'" :isWidget="true" />
</twig:Card> </twig:Card>
</div> </div>
<div class="flex flex-col gap-4"> <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 %} {% for movie in popular_movies %}
<twig:Poster imdbId="{{ movie.imdbId }}" <twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}" tmdbId="{{ movie.tmdbId }}"
@@ -32,7 +32,7 @@
/> />
{% endfor %} {% endfor %}
</twig:Card> </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 %} {% for movie in popular_tvshows %}
<twig:Poster imdbId="{{ movie.imdbId }}" <twig:Poster imdbId="{{ movie.imdbId }}"
tmdbId="{{ movie.tmdbId }}" tmdbId="{{ movie.tmdbId }}"

View File

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

View File

@@ -7,11 +7,11 @@
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2> <h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
<div class="flex flex-row w-full gap-2"> <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"> <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 %} {% 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 %} {% 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" /> <twig:ux:icon width="24" name="hugeicons:loading-01" />
</div> </div>
{% endif %} {% endif %}
@@ -127,20 +127,4 @@
</twig:Card> </twig:Card>
</div> </div>
</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 %} {% 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") }} {{ stimulus_target(controller, "list") }}
> >
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
{% for result in results.results %} <tr class="dark:bg-stone-600 overflow-hidden">
<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">
<th scope="col" <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 Size
</th> </th>
<th scope="col" <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 Resolution
</th> </th>
<th scope="col" <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 Codec
</th> </th>
<th scope="col" <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 Seeders
</th> </th>
<th scope="col" <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 Provider
</th> </th>
<th scope="col" <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 Language
</th> </th>
<th scope="col" <th scope="col"
class="px-4 py-4 leading-[32px] 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">
Actions
</th> </th>
</tr> </tr>
{% endfor %}
</thead> </thead>
<tbody class="flex-1 sm:flex-none"> <tbody>
{% for result in results.results %} {% for result in results.results %}
<tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow 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 %}> <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-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50"> <td id="size" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.size }} {{ result.size }}
</td> </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 }} {{ result.resolution }}
</td> </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 }} {{ result.codec }}
</td> </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 }} {{ result.seeders }}
</td> </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 }} {{ result.provider }}
</td> </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 }} {{ result.languageFlags|raw }}
</td> </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" <button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50"
{{ stimulus_controller('download_button', { {{ stimulus_controller('download_button', {
url: result.url, url: result.url,
title: results.media.title, title: results.media.title,
filename: result.filename, filename: result.filename,
mediaType: results.media.mediaType, mediaType: results.media.mediaType,
imdbId: results.media.imdbId ?? app.current_route_parameters.imdbId, imdbId: results.media.imdbId,
episodeId: results|episode_id_from_results episodeId: results|episode_id_from_results
}) }} }) }}
{{ stimulus_action('download_button', 'download', 'click') }} {{ stimulus_action('download_button', 'download', 'click') }}

View File

@@ -2,8 +2,20 @@
{% block title %}Preferences{% endblock %} {% block title %}Preferences{% endblock %}
{% block h2 %}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 %} {% 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"> <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> <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') }}"> <form id="media_preferences" class="flex flex-col max-w-64" name="media_preferences" method="post" action="{{ path('app_save_media_preferences') }}">