Compare commits
6 Commits
f4644d40ef
...
v0.38.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759f64ea22 | ||
|
|
cc88660c07 | ||
|
|
dbcc24c49f | ||
|
|
939b059872 | ||
|
|
f968e7e622 | ||
|
|
7958f50ff7 |
3
.env
3
.env
@@ -68,3 +68,6 @@ SENTRY_JS_URL=
|
|||||||
# - only include media originally
|
# - only include media originally
|
||||||
# produced in this language
|
# produced in this language
|
||||||
TMDB_ORIGINAL_LANGUAGE=en
|
TMDB_ORIGINAL_LANGUAGE=en
|
||||||
|
|
||||||
|
# Cache Torrentio Results
|
||||||
|
TORRENTIO_CACHE_RESULTS=true
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ parameters:
|
|||||||
sentry.dsn: '%env(SENTRY_DSN)%'
|
sentry.dsn: '%env(SENTRY_DSN)%'
|
||||||
sentry.javascript_url: '%env(SENTRY_JS_URL)%'
|
sentry.javascript_url: '%env(SENTRY_JS_URL)%'
|
||||||
|
|
||||||
|
# Torrentio
|
||||||
|
torrentio.cache_results: '%env(bool:TORRENTIO_CACHE_RESULTS)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Base;
|
|||||||
|
|
||||||
use App\Base\Dto\AppVersionDto;
|
use App\Base\Dto\AppVersionDto;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
|
||||||
final class ConfigResolver
|
final class ConfigResolver
|
||||||
@@ -14,6 +15,7 @@ final class ConfigResolver
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly DenormalizerInterface $denormalizer,
|
private readonly DenormalizerInterface $denormalizer,
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
|
||||||
#[Autowire(param: 'app.url')]
|
#[Autowire(param: 'app.url')]
|
||||||
private readonly ?string $appUrl = null,
|
private readonly ?string $appUrl = null,
|
||||||
@@ -62,6 +64,9 @@ final class ConfigResolver
|
|||||||
|
|
||||||
#[Autowire(param: 'sentry.javascript_url')]
|
#[Autowire(param: 'sentry.javascript_url')]
|
||||||
private ?string $sentryJavascriptUrl = null,
|
private ?string $sentryJavascriptUrl = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'torrentio.cache_results')]
|
||||||
|
private ?bool $torrentioCacheResults = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function validate(): bool
|
public function validate(): bool
|
||||||
@@ -110,6 +115,11 @@ final class ConfigResolver
|
|||||||
return $this->authOidcBypassFormLogin;
|
return $this->authOidcBypassFormLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isTorrentioCacheEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->torrentioCacheResults;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAppVersion(): AppVersionDto
|
public function getAppVersion(): AppVersionDto
|
||||||
{
|
{
|
||||||
$matches = [];
|
$matches = [];
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use App\Download\Action\Command\DownloadSeasonCommand;
|
|||||||
use App\Download\Action\Result\DownloadMediaResult;
|
use App\Download\Action\Result\DownloadMediaResult;
|
||||||
use App\Download\Action\Result\DownloadSeasonResult;
|
use App\Download\Action\Result\DownloadSeasonResult;
|
||||||
use App\Download\DownloadOptionEvaluator;
|
use App\Download\DownloadOptionEvaluator;
|
||||||
|
use App\Download\Framework\Entity\Download;
|
||||||
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
@@ -32,7 +34,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
private UserRepository $userRepository,
|
private UserRepository $userRepository, private DownloadRepository $downloadRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
@@ -68,13 +70,16 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
if (null !== $result) {
|
if (null !== $result) {
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode->episodeNumber);
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode->episodeNumber);
|
||||||
|
$download = $this->createDownload($command, $result->url, $series->title, $result->filename, $episode->episodeNumber);
|
||||||
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Created Download entity with id ' . $download->getId());
|
||||||
$downloadCommand = new DownloadMediaCommand(
|
$downloadCommand = new DownloadMediaCommand(
|
||||||
$result->url,
|
$download->getUrl(),
|
||||||
$series->title,
|
$download->getTitle(),
|
||||||
$result->filename,
|
$download->getFilename(),
|
||||||
'tvshows',
|
$download->getMediaType(),
|
||||||
$command->imdbId,
|
$download->getImdbId(),
|
||||||
$command->userId,
|
$download->getUser()->getId(),
|
||||||
|
$download->getId()
|
||||||
);
|
);
|
||||||
$this->bus->dispatch($downloadCommand);
|
$this->bus->dispatch($downloadCommand);
|
||||||
$downloadCommands[] = $downloadCommand;
|
$downloadCommands[] = $downloadCommand;
|
||||||
@@ -90,19 +95,27 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDownloadedEpisodes(string $title)
|
private function createDownload(DownloadSeasonCommand $command, string $url, string $title, string $filename, int $episodeNumber): Download
|
||||||
{
|
{
|
||||||
// Check current episodes
|
$download = new Download();
|
||||||
$downloadedEpisodes = $this->mediaFiles
|
$download->setUrl($url);
|
||||||
->getEpisodes($title)
|
$download->setTitle($title);
|
||||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
$download->setFilename($filename);
|
||||||
->filter(fn ($episode) =>
|
$download->setImdbId($command->imdbId);
|
||||||
property_exists($episode, 'episode')
|
$download->setMediaType(MediaType::TvShow->value);
|
||||||
&& property_exists($episode, 'season')
|
$download->setEpisodeId($this->getEpisodeNumber($command->season, $episodeNumber));
|
||||||
&& null !== $episode->episodeNumber
|
$download->setUser($this->userRepository->find($command->userId));
|
||||||
&& null !== $episode->season
|
$this->downloadRepository->getEntityManager()->persist($download);
|
||||||
)
|
$this->downloadRepository->getEntityManager()->flush();
|
||||||
->rekey(fn($episode) => $episode->episodeNumber);
|
return $download;
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
}
|
||||||
|
|
||||||
|
private function getEpisodeNumber(int $season, int $episode): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
"S%sE%s",
|
||||||
|
str_pad($season, 2, "0", STR_PAD_LEFT),
|
||||||
|
str_pad($episode, 2, "0", STR_PAD_LEFT)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ class DownloadOptionEvaluator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->validateSize($result, $filter)) {
|
// todo: This is arbitrary- revisit in the future
|
||||||
|
//if (false === $this->validateSize($result, $filter)) {
|
||||||
|
// return false;
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (false === $this->validateDownloadUrl($result->url)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,15 +52,15 @@ class DownloadOptionEvaluator
|
|||||||
$valid = false;
|
$valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $filter->codec && in_array($result->codec, $filter->codec)) {
|
if (null !== $filter->codec && !in_array($result->codec, $filter->codec)) {
|
||||||
$valid = false;
|
$valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $filter->quality && in_array($result->quality, $filter->quality)) {
|
if (null !== $filter->quality && !in_array($result->quality, $filter->quality)) {
|
||||||
$valid = false;
|
$valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $filter->provider && in_array($result->provider, $filter->provider)) {
|
if (null !== $filter->provider && !in_array($result->provider, $filter->provider)) {
|
||||||
$valid = false;
|
$valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,4 +84,18 @@ class DownloadOptionEvaluator
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateDownloadUrl(string $downloadUrl)
|
||||||
|
{
|
||||||
|
$badFileLocations = [
|
||||||
|
'https://torrentio.strem.fun/videos/failed_infringement_v2.mp4' => 'Removed for Copyright Infringement.',
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = get_headers($downloadUrl, true);
|
||||||
|
if (array_key_exists($headers['Location'], $badFileLocations)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,4 +123,16 @@ class DownloadRepository extends ServiceEntityRepository
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function badDownloadUrls()
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('d')
|
||||||
|
->select('d.url')
|
||||||
|
->andWhere('d.status = :status')
|
||||||
|
->andWhere('d.progress = 100')
|
||||||
|
->setParameter('status', 'Failed')
|
||||||
|
->distinct()
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Client;
|
namespace App\Torrentio\Client;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Torrentio\Result\ResultFactory;
|
use App\Torrentio\Result\ResultFactory;
|
||||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class Torrentio
|
class Torrentio
|
||||||
{
|
{
|
||||||
|
private array $badDownloadUrls = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HttpClient $client,
|
private readonly HttpClient $client,
|
||||||
) {}
|
private readonly DownloadRepository $downloadRepository, private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
$badDownloadUrls = $this->downloadRepository->badDownloadUrls();
|
||||||
|
$this->badDownloadUrls = Map::from($badDownloadUrls)
|
||||||
|
->map(fn ($url) => $url['url'])
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function search(string $imdbCode, string $type, bool $parseResults = true): array
|
public function search(string $imdbCode, string $type, bool $parseResults = true): array
|
||||||
{
|
{
|
||||||
@@ -47,6 +58,16 @@ class Torrentio
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$url = explode('/', $stream['url']);
|
||||||
|
$filename = urldecode(end($url));
|
||||||
|
$url[count($url) - 1] = $filename;
|
||||||
|
$url = implode('/', $url);
|
||||||
|
|
||||||
|
if (in_array($stream['url'], $this->badDownloadUrls)) {
|
||||||
|
$this->logger->warning($stream['url'] . ' was skipped because it was identified as a bad download url.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
array_key_exists('behaviorHints', $stream) &&
|
array_key_exists('behaviorHints', $stream) &&
|
||||||
array_key_exists('bingeGroup', $stream['behaviorHints'])
|
array_key_exists('bingeGroup', $stream['behaviorHints'])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Framework\Controller;
|
namespace App\Torrentio\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Base\ConfigResolver;
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
@@ -27,6 +28,7 @@ final class WebController extends AbstractController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
|
private readonly ConfigResolver $configResolver,
|
||||||
private readonly Broadcaster $broadcaster,
|
private readonly Broadcaster $broadcaster,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -40,10 +42,14 @@ final class WebController extends AbstractController
|
|||||||
$input->imdbId
|
$input->imdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (true === $this->configResolver->isTorrentioCacheEnabled()) {
|
||||||
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
|
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
|
||||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||||
return $this->getMovieOptionsHandler->handle($input->toCommand());
|
return $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->headers->get('Turbo-Frame')) {
|
if ($request->headers->get('Turbo-Frame')) {
|
||||||
return $this->sendFragmentResponse($results, $request);
|
return $this->sendFragmentResponse($results, $request);
|
||||||
@@ -66,10 +72,14 @@ final class WebController extends AbstractController
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (true === $this->configResolver->isTorrentioCacheEnabled()) {
|
||||||
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
$results = $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));
|
||||||
return $this->getTvShowOptionsHandler->handle($input->toCommand());
|
return $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->headers->get('Turbo-Frame')) {
|
if ($request->headers->get('Turbo-Frame')) {
|
||||||
return $this->sendFragmentResponse($results, $request);
|
return $this->sendFragmentResponse($results, $request);
|
||||||
|
|||||||
Reference in New Issue
Block a user