Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 662e2600f6 | |||
| aa042e8275 | |||
| 57498b1abf | |||
| fed1e1e122 | |||
| 9eef567974 | |||
| 070723581a | |||
| f3a5c2012e |
47
migrations/Version20250831013403.php
Normal file
47
migrations/Version20250831013403.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250831013403 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE sessions
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -19,7 +19,8 @@ class GetMediaInfoHandler implements HandlerInterface
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
$relatedMedia = $this->tmdb->relatedMedia($media->tmdbId, $command->mediaType);
|
||||
|
||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
||||
return new GetMediaInfoResult($media, $relatedMedia, $command->season, $command->episode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class GetMediaInfoResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public array $relatedMedia,
|
||||
public ?int $season,
|
||||
public ?int $episode,
|
||||
) {}
|
||||
|
||||
@@ -7,9 +7,6 @@ use App\Search\Action\Handler\SearchHandler;
|
||||
use App\Search\Action\Input\GetMediaInfoInput;
|
||||
use App\Search\Action\Input\SearchInput;
|
||||
use App\Search\Action\Result\RedirectToMediaResult;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
@@ -247,6 +247,21 @@ class Tmdb
|
||||
return $series;
|
||||
}
|
||||
|
||||
public function relatedMedia(string $tmdbId, string $mediaType, int $maxResults = 6)
|
||||
{
|
||||
$repos = [
|
||||
'movies' => $this->movieRepository,
|
||||
'tvshows' => $this->tvRepository,
|
||||
];
|
||||
|
||||
$results = $repos[$mediaType]->getRecommendations($tmdbId);
|
||||
return Map::from(array_values($results->toArray()))
|
||||
->slice(0, 6)
|
||||
->map(function ($result) use ($mediaType) {
|
||||
return $this->parseResult($result, $mediaType);
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function mediaDetails(string $id, string $type)
|
||||
{
|
||||
$id = $this->find($id);
|
||||
|
||||
55
src/Torrentio/Client/HttpClient.php
Normal file
55
src/Torrentio/Client/HttpClient.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Torrentio\Client;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
class HttpClient
|
||||
{
|
||||
private GuzzleClient $client;
|
||||
|
||||
private string $baseUrl = 'https://torrentio.strem.fun/realdebrid=%s/';
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
|
||||
private TagAwareCacheInterface $cache,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
$this->client = new GuzzleClient([
|
||||
'base_uri' => sprintf($this->baseUrl, $this->realDebridKey),
|
||||
]);
|
||||
}
|
||||
|
||||
public function get(string $imdbId, array $cacheTags = []): array
|
||||
{
|
||||
$cacheKey = str_replace(":", ".", "torrentio.{$imdbId}");
|
||||
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $cacheTags) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
if (count($cacheTags) > 0) {
|
||||
$item->tag($cacheTags);
|
||||
}
|
||||
try {
|
||||
$response = $this->client->get("stream/movie/$imdbId.json");
|
||||
return json_decode(
|
||||
$response->getBody()->getContents(),
|
||||
true
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
dd($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 [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,59 +2,19 @@
|
||||
|
||||
namespace App\Torrentio\Client;
|
||||
|
||||
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\ItemInterface;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
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';
|
||||
|
||||
private string $searchUrl;
|
||||
|
||||
private Client $client;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
|
||||
private TagAwareCacheInterface $cache,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
|
||||
$this->client = new Client([
|
||||
'base_uri' => $this->searchUrl,
|
||||
]);
|
||||
}
|
||||
private readonly HttpClient $client,
|
||||
) {}
|
||||
|
||||
public function search(string $imdbCode, string $type, bool $parseResults = true): array
|
||||
{
|
||||
$cacheKey = "torrentio.{$imdbCode}";
|
||||
|
||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$item->tag(['torrentio', $type, $imdbCode]);
|
||||
try {
|
||||
$response = $this->client->get("$this->searchUrl/$imdbCode.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 [];
|
||||
});
|
||||
$cacheTags = ['torrentio', $type, $imdbCode];
|
||||
$results = $this->client->get($imdbCode, $cacheTags);
|
||||
|
||||
if (true === $parseResults) {
|
||||
return $this->parse($results);
|
||||
@@ -65,26 +25,8 @@ class Torrentio
|
||||
|
||||
public function fetchEpisodeResults(string $imdbId, int $season, int $episode, bool $parseResults = true): array
|
||||
{
|
||||
$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));
|
||||
$item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
|
||||
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 [];
|
||||
});
|
||||
$cacheTags = ['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"];
|
||||
$results = $this->client->get("$imdbId:$season:$episode", $cacheTags);
|
||||
|
||||
if (null === $results) {
|
||||
throw new TorrentioRateLimitException();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<twig:SearchBar />
|
||||
<div class="md:flex md:items-center md:gap-12">
|
||||
<nav aria-label="Global" class="md:block">
|
||||
<ul class="flex items-center gap-6 text-sm">
|
||||
<ul class="ml-4 flex items-end md:items-center md:gap-6 text-sm">
|
||||
<li>
|
||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
||||
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{% endfor %}
|
||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||
<tr id="monitor_view_all">
|
||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-gray-400 dark:bg-gray-700 uppercase text-xs font-medium text-center text-black dark:text-white min-w-[50ch] max-w-[50ch] truncate">
|
||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
||||
|
||||
{% block h2 %}Media Results{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4 flex flex-col grow gap-4">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
||||
<div class="flex flex-row w-full gap-2">
|
||||
<twig:Card title="" class="w-full" 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">
|
||||
@@ -19,7 +20,7 @@
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||
{{ results.media.title }} ({{ results.media.year }})
|
||||
{{ results.media.title }} ({{ results.media.year|date('Y') }})
|
||||
</h3>
|
||||
|
||||
{% if results.media.mediaType == "tvshows" %}
|
||||
@@ -95,12 +96,35 @@
|
||||
{% elseif "tvshows" == results.media.mediaType %}
|
||||
<twig:TvEpisodeList
|
||||
results="results"
|
||||
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
||||
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer" :episodeNumber="results.episode"
|
||||
loading="defer"
|
||||
:imdbId="results.media.imdbId"
|
||||
:season="results.season"
|
||||
:perPage="20"
|
||||
:pageNumber="1"
|
||||
:tmdbId="results.media.tmdbId"
|
||||
:title="results.media.title"
|
||||
:episodeNumber="results.episode"
|
||||
/>
|
||||
{% endif %}
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
||||
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
||||
{% for media in results.relatedMedia %}
|
||||
<twig:Poster imdbId="{{ media.imdbId }}"
|
||||
tmdbId="{{ media.tmdbId }}"
|
||||
title="{{ media.title }}"
|
||||
description="{{ media.description }}"
|
||||
image="{{ media.poster }}"
|
||||
year="{{ media.year }}"
|
||||
mediaType="{{ media.mediaType }}"
|
||||
/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</twig:Card>
|
||||
</div>
|
||||
<style>
|
||||
html,
|
||||
|
||||
Reference in New Issue
Block a user