feat: media result page

This commit is contained in:
2025-04-21 11:30:18 -05:00
parent fb4b7dc71e
commit 77907601f8
24 changed files with 844 additions and 19 deletions

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Torrentio\Result;
use App\Util\CountryCodes;
use Nihilarr\PTN;
class ResultFactory
{
public static function map(
string $url,
string $title,
string $bingeGroup = "-"
) {
$ptn = (object) (new PTN())->parse($title);
return new TorrentioResult(
self::trimTitle($title),
$url,
self::setSize($title),
self::setSeeders($title),
self::setProvider($title),
self::setEpisode($title),
$ptn->season ?? "-",
$bingeGroup,
$ptn->resolution ?? "-",
$ptn->codec ?? "-",
$ptn,
substr(base64_encode($url), strlen($url) - 10),
$ptn->episode ?? "-",
self::setLanguages($title),
self::setLanguageFlags($title),
false
);
}
public static function setSize(string $title): string
{
$sizeMatch = [];
preg_match('/(\d+\.?\d+ )(GB|MB)/', $title, $sizeMatch);
return $sizeMatch[0] ?? "-";
}
private static function setSeeders(string $title): string
{
$emoji = \Emoji\detect_emoji($title);
return intval(
grapheme_substr($title, $emoji[0]['grapheme_offset'] + 1, $emoji[1]['grapheme_offset'] - $emoji[0]['grapheme_offset'])
);
}
private static function setProvider(string $title): string
{
$emoji = \Emoji\detect_emoji($title);
$provider = trim(
grapheme_substr($title, $emoji[2]['grapheme_offset'] + 1, strlen($title) - $emoji[1]['grapheme_offset'])
);
$providerParts = explode("\n", $provider);
return $providerParts[0];
}
private static function setLanguageFlags(string $title): string
{
$emoji = \Emoji\detect_emoji($title);
$provider = trim(
grapheme_substr($title, $emoji[2]['grapheme_offset'] + 1, strlen($title) - $emoji[1]['grapheme_offset'])
);
$providerParts = explode("\n", $provider);
if (array_key_exists(1, $providerParts)) {
return $providerParts[1];
} else {
return "&#x1f1fa;&#x1f1f8;";
}
}
public static function setLanguages(string $title): array
{
$emoji = \Emoji\detect_emoji($title);
$flags = array_filter($emoji, function ($emoji) {
return str_starts_with($emoji['short_name'], 'flag-');
});
$languages = array_map(function ($flag) {
return CountryCodes::convertFromAbbr(strtoupper(substr($flag['short_name'], strlen('flag-'))));
},
$flags);
if (count($languages) > 0) {
return array_values($languages);
} else {
return ["English"];
}
}
private static function setEpisode(string $title)
{
$value = [];
preg_match('/[sS]\d\d[eE]\d\d/', $title, $value);
return array_key_exists(0, $value) ? strtoupper($value[0]) : "n/a";
}
private static function trimTitle(string $title)
{
$emoji = \Emoji\detect_emoji($title);
return trim(grapheme_substr($title, 0, $emoji[0]['grapheme_offset']));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Torrentio\Result;
class TorrentioResult
{
public function __construct(
public ?string $title = "-",
public ?string $url = "-",
public ?string $size = "-",
public ?string $seeders = "-",
public ?string $provider = "-",
public ?string $episode = "-",
public ?string $season = "-",
public ?string $bingeGroup = "-",
public ?string $resolution = "-",
public ?string $codec = "-",
public object|array $ptn = [],
public ?string $key = "-",
public ?string $episodeNumber = "-",
public ?array $languages = [],
public ?string $languageFlags = "-",
public ?bool $selected = false,
) {}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Torrentio\Rule\DownloadOptionFilter;
use App\Torrentio\Result\ResultFactory;
class Resolution
{
public function __construct(
public string $expectedValue
) {}
public function __invoke(ResultFactory $result): bool
{
return $result->resolution === $this->expectedValue;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Torrentio\Rule;
class RuleEngine
{
private array $rules = [];
public function addRule(callable $rule): void
{
$this->rules[] = $rule;
}
public function validateAny($fact): bool
{
foreach ($this->rules as $rule) {
if ($rule($fact)) {
return true;
}
}
return false;
}
public function validateAll($fact): bool
{
foreach ($this->rules as $rule) {
if (!$rule($fact)) {
return false;
}
}
return true;
}
}

117
src/Torrentio/Torrentio.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
namespace App\Torrentio;
use App\Torrentio\Rule\DownloadOptionFilter\Resolution;
use App\Torrentio\Rule\RuleEngine;
use App\Torrentio\Result\ResultFactory;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* ToDo: Fix
*/
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=yts,eztv,rarbg,1337x,thepiratebay,kickasstorrents,torrentgalaxy,magnetdl,horriblesubs|sort=qualitysize|qualityfilter=480p,cam,unknown|debridoptions=nodownloadlinks|realdebrid=QYYBR7OSQ4VEFKWASDEZ2B4VO67KHUJY6IWOT7HHA7ATXO7QCYDQ/stream/{imdbCode}.json';
private string $searchUrl;
public function __construct(
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
private CacheInterface $cache,
) {
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
}
public function search(string $imdbCode, string $type, array $filter = []): array
{
$cacheKey = "torrentio.{$imdbCode}";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode) {
$item->expiresAt(new \DateTimeImmutable("today 11:59 pm"));
$response = file_get_contents(str_replace('{imdbCode}', $imdbCode, $this->searchUrl));
return json_decode(
$response,
true
);
});
return $this->parse($results, $filter);
}
public function searchBySeriesSeason(MediaResult $series): MediaResult
{
$imdbCode = $series->imdbId;
// foreach ($series->episodes as $season => $episodes) {
// foreach ($episodes as $key => $episode) {
// $cacheKey = "torrentio.$series->imdbId.$season.{$episode['episode_number']}";
// $downloadOptions = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $season, $episode) {
// $item->expiresAt(new \DateTimeImmutable("today 11:59 pm"));
// $response = file_get_contents(str_replace('{imdbCode}', "$imdbCode:$season:{$episode['episode_number']}", $this->searchUrl));
// return json_decode(
// $response,
// true
// );
// });
// $series->episodes[$season][$key]['download_options'] = $this->parse($downloadOptions, []);
// }
// }
return $series;
}
public function fetchEpisodeResults(string $imdbId, int $season, int $episode): array
{
$cacheKey = "torrentio.$imdbId.$season.$episode";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
$item->expiresAt(new \DateTimeImmutable("today 11:59 pm"));
$response = file_get_contents(str_replace('{imdbCode}', "$imdbId:$season:$episode", $this->searchUrl));
return json_decode(
$response,
true
);
});
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));
}
}
$results = [];
foreach ($data['streams'] as $stream) {
if (!str_starts_with($stream['url'], "https")) {
continue;
}
if (
array_key_exists('behaviorHints', $stream) &&
array_key_exists('bingeGroup', $stream['behaviorHints'])
) {
$bingeGroup = $stream['behaviorHints']['bingeGroup'];
} else {
$bingeGroup = '-';
}
$result = ResultFactory::map(
$stream['url'],
$stream['title'],
$bingeGroup
);
if ($ruleEngine->validateAll($result)) {
$results[] = $result;
}
}
return $results;
}
}