diff --git a/assets/controllers/tv_results_controller.js b/assets/controllers/tv_results_controller.js index 497795f..c45c87d 100644 --- a/assets/controllers/tv_results_controller.js +++ b/assets/controllers/tv_results_controller.js @@ -32,19 +32,28 @@ export default class extends Controller { async setOptions() { if (this.optionsLoaded === false) { this.optionsLoaded = true; - await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`) - .then(res => res.text()) - .then(response => { - this.listContainerTarget.innerHTML = response; - this.options = this.element.querySelectorAll('tbody tr'); - if (this.options.length > 0) { - this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue); - this.options[0].querySelector('input[type="checkbox"]').checked = true; - } else { - this.episodeSelectorTarget.disabled = true; - } - this.loadingIconOutlet.increaseCount(); - }); + let response; + + try { + response = await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`) + } catch (error) { + console.log('There was an error', error); + } + + if (response?.ok) { + response = await response.text() + this.listContainerTarget.innerHTML = response; + this.options = this.element.querySelectorAll('tbody tr'); + if (this.options.length > 0) { + this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue); + this.options[0].querySelector('input[type="checkbox"]').checked = true; + } else { + this.episodeSelectorTarget.disabled = true; + } + this.loadingIconOutlet.increaseCount(); + } else { + console.log(`HTTP Response Code: ${response?.status}`) + } } } diff --git a/composer.json b/composer.json index 2501a84..389d5c9 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", "dragonmantank/cron-expression": "^3.4", + "guzzlehttp/guzzle": "^7.9", "league/pipeline": "^1.1", "nesbot/carbon": "^3.9", "nihilarr/parse-torrent-name": "^0.0.1", diff --git a/composer.lock b/composer.lock index 6f46c6b..43097d6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8b5e39f9d73a6ace2b9e39240186b4f", + "content-hash": "3b0840f4e60d44d341c934f6ca153944", "packages": [ { "name": "1tomany/rich-bundle", @@ -1947,6 +1947,331 @@ ], "time": "2025-04-04T17:19:27+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, { "name": "lcobucci/jwt", "version": "5.5.0", @@ -3527,6 +3852,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "runtime/frankenphp-symfony", "version": "0.2.0", diff --git a/src/Controller/TorrentioController.php b/src/Controller/TorrentioController.php index ad40ee7..817c047 100644 --- a/src/Controller/TorrentioController.php +++ b/src/Controller/TorrentioController.php @@ -6,6 +6,7 @@ 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\Exception\TorrentioRateLimitException; use App\Util\Broadcaster; use Carbon\Carbon; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -52,13 +53,23 @@ final class TorrentioController extends AbstractController $input->episode, ); - 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, - ]); - }); + 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) { + 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')] diff --git a/src/Torrentio/Client/Torrentio.php b/src/Torrentio/Client/Torrentio.php index 8eb9b95..eb50c5e 100644 --- a/src/Torrentio/Client/Torrentio.php +++ b/src/Torrentio/Client/Torrentio.php @@ -6,21 +6,30 @@ use App\Torrentio\Client\Rule\DownloadOptionFilter\Resolution; use App\Torrentio\Client\Rule\RuleEngine; use App\Torrentio\Result\ResultFactory; use Carbon\Carbon; +use App\Torrentio\Exception\TorrentioRateLimitException; +use GuzzleHttp\Client; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; class Torrentio { - private string $baseUrl = 'https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={realDebridKey}/stream/movie/{imdbCode}.json'; + private string $baseUrl = 'https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={realDebridKey}/stream/movie'; private string $searchUrl; + private Client $client; + public function __construct( #[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey, private CacheInterface $cache, + private LoggerInterface $logger, ) { $this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl); + $this->client = new Client([ + 'base_uri' => $this->searchUrl, + ]); } public function search(string $imdbCode, string $type, array $filter = []): array @@ -44,13 +53,27 @@ class Torrentio $cacheKey = "torrentio.$imdbId.$season.$episode"; $results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) { $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); - $response = file_get_contents(str_replace('{imdbCode}', "$imdbId:$season:$episode", $this->searchUrl)); - return json_decode( - $response, - true - ); + try { + $response = $this->client->get("$this->searchUrl/$imdbId:$season:$episode.json"); + return json_decode( + $response->getBody()->getContents(), + true + ); + } catch (\Throwable $exception) { + if ($exception->getCode() === 429) { + $this->logger->warning("> [TorrentioClient] Rate limit exceeded"); + return null; + } + } + + $this->logger->error("> [TorrentioClient] Request error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents()); + return []; }); + if (null === $results) { + throw new TorrentioRateLimitException(); + } + return $this->parse($results, []); } diff --git a/src/Torrentio/Exception/TorrentioRateLimitException.php b/src/Torrentio/Exception/TorrentioRateLimitException.php new file mode 100644 index 0000000..0ff2c58 --- /dev/null +++ b/src/Torrentio/Exception/TorrentioRateLimitException.php @@ -0,0 +1,11 @@ + 300) { + $text = substr($text, 0, 300) . '...'; + } + return $text; + } + #[AsTwigFilter('filesize')] public function type(string|int $size) { diff --git a/templates/components/TvEpisodeList.html.twig b/templates/components/TvEpisodeList.html.twig index c36f98b..f2d6731 100644 --- a/templates/components/TvEpisodeList.html.twig +++ b/templates/components/TvEpisodeList.html.twig @@ -28,7 +28,7 @@
{{ episode['overview'] }}
+{{ episode['overview']|truncate }}