Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eaa120257 | |||
| d6cbb53da6 | |||
| bd47107399 | |||
| ac97fdd08f |
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,16 @@ class MonitorDispatcher
|
|||||||
$this->bus->dispatch(new $command($monitor->getId()));
|
$this->bus->dispatch(new $command($monitor->getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function cleanupStuckMonitors(): void
|
||||||
|
{
|
||||||
|
$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()) > 6) {
|
||||||
|
$monitor->setStatus('Active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
116
src/Torrentio/Framework/Controller/ApiController.php
Normal file
116
src/Torrentio/Framework/Controller/ApiController.php
Normal 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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
Title
|
Title
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ?? "hidden" }}">
|
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
|
Filename
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ?? "hidden" }}">
|
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
|
Media type
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
Progress
|
Progress
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="hidden md:table-cell 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">
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -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,11 +11,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] {{ isWidget == true ?? "hidden" }} truncate">
|
<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 }}
|
{{ download.filename }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate {{ isWidget == true ?? "hidden" }}">
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<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">
|
||||||
|
|||||||
@@ -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') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user