Compare commits

...

9 Commits

21 changed files with 292 additions and 67 deletions

View File

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

View File

@@ -64,6 +64,16 @@ dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards; animation: fade-out 200ms forwards;
} }
.r-tablecell {
display: none;
}
@media screen and (min-width: 768px) {
.r-tablecell {
display: inline-table;
}
}
.options-table { .options-table {
display: flex; display: flex;

View File

@@ -29,3 +29,11 @@ controllersMonitor:
type: attribute type: attribute
defaults: defaults:
schemes: ['https'] schemes: ['https']
controllersTorrentio:
resource:
path: ../src/Torrentio/Framework/Controller
namespace: App\Torrentio\Framework\Controller
type: attribute
defaults:
schemes: ['https']

View File

@@ -40,4 +40,11 @@ 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,6 +5,7 @@ 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;
@@ -30,9 +31,9 @@ final class IndexController extends AbstractController
} }
#[Route('/test', name: 'app_test')] #[Route('/test', name: 'app_test')]
public function test() public function test(MonitorDispatcher $dispatcher): Response
{ {
$result = $this->monitorTvShowHandler->handle(new MonitorTvShowCommand(355)); $dispatcher();
return $this->json($result); return new Response();
} }
} }

View File

@@ -6,8 +6,10 @@ 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;
@@ -50,4 +52,15 @@ 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,6 +4,7 @@ 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;
@@ -148,7 +149,7 @@ class Monitor
public function getLastSearch(): ?\DateTimeInterface public function getLastSearch(): ?\DateTimeInterface
{ {
return $this->lastSearch; return Carbon::parse($this->lastSearch);
} }
public function setLastSearch(?\DateTimeInterface $lastSearch): static public function setLastSearch(?\DateTimeInterface $lastSearch): static

View File

@@ -7,6 +7,7 @@ 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;
@@ -23,6 +24,8 @@ 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,
@@ -41,4 +44,18 @@ 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

@@ -32,7 +32,7 @@ class Torrentio
]); ]);
} }
public function search(string $imdbCode, string $type, array $filter = []): array public function search(string $imdbCode, string $type, bool $parseResults = true): array
{ {
$cacheKey = "torrentio.{$imdbCode}"; $cacheKey = "torrentio.{$imdbCode}";
@@ -56,10 +56,14 @@ class Torrentio
return []; return [];
}); });
return $this->parse($results, $filter); if (true === $parseResults) {
return $this->parse($results);
}
return $results;
} }
public function fetchEpisodeResults(string $imdbId, int $season, int $episode): array public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): 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->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
@@ -86,18 +90,15 @@ class Torrentio
throw new TorrentioRateLimitException(); throw new TorrentioRateLimitException();
} }
return $this->parse($results, []); if (true === $parseResults) {
} return $this->parse($results);
public function parse(array $data, array $filter): array
{
$ruleEngine = new RuleEngine();
foreach ($filter as $rule => $value) {
if ('resolution' === $rule) {
$ruleEngine->addRule(new Resolution($value));
}
} }
return $results;
}
public function parse(array $data): array
{
$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")) {
@@ -119,9 +120,7 @@ class Torrentio
$bingeGroup $bingeGroup
); );
if ($ruleEngine->validateAll($result)) { $results[] = $result;
$results[] = $result;
}
} }
return $results; return $results;

View File

@@ -0,0 +1,116 @@
<?php
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 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
{
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,
]);
});
}
}

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Controller; namespace App\Torrentio\Framework\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;
@@ -16,7 +16,7 @@ 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;
final class TorrentioController extends AbstractController final class WebController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler, private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
@@ -54,13 +54,13 @@ final class TorrentioController extends AbstractController
); );
try { try {
return $cache->get($cacheId, function (ItemInterface $item) use ($input) { // return $cache->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));
$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

@@ -11,21 +11,17 @@
<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 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate"> class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
Title Title
</th> </th>
<th scope="col"
{% if this.isWidget == false %} 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 scope="col" Filename
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate"> </th>
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 {{ isWidget == true ? "hidden" : "r-tablecell" }}">
<th scope="col" Media type
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate"> </th>
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
@@ -38,7 +34,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="{{ this.isWidget }}" /> <twig:DownloadListRow download="{{ download }}" isWidget="{{ 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" class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate"
> >
{{ download.title }} {{ download.title }}
</a> </a>
@@ -11,14 +11,13 @@
{% endif %} {% endif %}
</td> </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] {{ isWidget == true ? "hidden" : "r-tablecell" }} truncate">
<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 }}
{{ download.filename }} </td>
</td>
<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 {{ isWidget == true ? "hidden" : "r-tablecell" }}">
{{ 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 %}
@@ -32,7 +31,7 @@
<twig:StatusBadge color="green" status="Complete" /> <twig:StatusBadge color="green" status="Complete" />
{% endif %} {% endif %}
</td> </td>
<td id="action_buttons_{{ download.id }}" class="px-6 py-4 flex flex-row items-center"> <td id="hidden md:table-cell 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

@@ -13,7 +13,11 @@
<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 class="md:hidden"> <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> <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> </li>
</ul> </ul>

View File

@@ -16,32 +16,32 @@
ID ID
</th> </th>
<th scope="col" <th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase"> class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
Search Count Search Count
</th> </th>
<th scope="col" <th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase"> class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
Created at Created at
</th> </th>
<th scope="col" <th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase"> class="hidden md:table-cell 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="px-6 py-3 text-start text-xs font-medium uppercase"> class="hidden md:table-cell 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></th> <th class="hidden md:table-cell"></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" /> <twig:MonitorListRow :monitor="monitor" isWidget="{{ this.isWidget }}" />
{% 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="px-6 py-4 whitespace-nowrap text-sm text-gray-800"> <td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ monitor.searchCount }} {{ monitor.searchCount }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800"> <td class="hidden md:table-cell 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="px-6 py-4 whitespace-nowrap text-sm text-gray-800"> <td class="hidden md:table-cell 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="px-6 py-4 whitespace-nowrap text-sm text-gray-800"> <td class="hidden md:table-cell 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="px-6 py-4 flex flex-row align-middle justify-center"> <td class="hidden md:table-cell 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 {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950"> <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">
<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

@@ -12,7 +12,7 @@
<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:text-gray-50"> <p class="hidden md:block md:text-gray-50">
{{ description }} {{ description }}
</p> </p>
</div> </div>

View File

@@ -63,7 +63,7 @@
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, imdbId: results.media.imdbId ?? app.current_route_parameters.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

@@ -3,7 +3,7 @@
{% block h2 %}Preferences{% endblock %} {% block h2 %}Preferences{% endblock %}
{% block body %} {% block body %}
<div class="p-4 flex flex-row gap-2"> <div class="p-4 flex flex-col md: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') }}">