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

@@ -8,7 +8,9 @@
"ext-ctype": "*",
"ext-iconv": "*",
"1tomany/rich-bundle": "^1.8",
"nihilarr/parse-torrent-name": "^0.0.1",
"nyholm/psr7": "*",
"p3k/emoji-detector": "^1.2",
"php-tmdb/api": "^4.1",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",

93
composer.lock generated
View File

@@ -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": "52ea99b6f072a3dba10316c24060b886",
"content-hash": "e8c968ee6b83d42fa44746ec5e4d303d",
"packages": [
{
"name": "1tomany/data-uri",
@@ -251,6 +251,50 @@
},
"time": "2025-04-07T20:06:18+00:00"
},
{
"name": "nihilarr/parse-torrent-name",
"version": "v0.0.1",
"source": {
"type": "git",
"url": "https://gitlab.com/nihilarr/parse-torrent-name.git",
"reference": "0d8ddb6c91b33845d2e26677304ea91f69c90319"
},
"dist": {
"type": "zip",
"url": "https://gitlab.com/api/v4/projects/nihilarr%2Fparse-torrent-name/repository/archive.zip?sha=0d8ddb6c91b33845d2e26677304ea91f69c90319",
"reference": "0d8ddb6c91b33845d2e26677304ea91f69c90319",
"shasum": ""
},
"require": {
"php": ">=5.3.24"
},
"type": "library",
"autoload": {
"psr-4": {
"Nihilarr\\": "PTN/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Drew Smith",
"homepage": "https://www.nihilarr.com"
}
],
"description": "Extract media information from torrent-like filename",
"homepage": "https://gitlab.com/nihilarr/parse-torrent-name",
"keywords": [
"library",
"parse",
"parser",
"ptn",
"torrent"
],
"time": "2018-04-23T01:48:14+00:00"
},
{
"name": "nyholm/psr7",
"version": "1.8.2",
@@ -519,6 +563,53 @@
},
"time": "2021-05-22T15:57:08+00:00"
},
{
"name": "p3k/emoji-detector",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/aaronpk/emoji-detector-php.git",
"reference": "dce4638e215622181d272f08145a3f97b735b1c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aaronpk/emoji-detector-php/zipball/dce4638e215622181d272f08145a3f97b735b1c7",
"reference": "dce4638e215622181d272f08145a3f97b735b1c7",
"shasum": ""
},
"require": {
"ext-intl": "*",
"ext-mbstring": "*",
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"type": "library",
"autoload": {
"files": [
"src/Emoji.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aaron Parecki",
"email": "aaron@parecki.com",
"homepage": "https://aaronparecki.com/"
}
],
"description": "Detect and return all emoji found in a string",
"homepage": "https://github.com/aaronpk/emoji-detector-php",
"support": {
"issues": "https://github.com/aaronpk/emoji-detector-php/issues",
"source": "https://github.com/aaronpk/emoji-detector-php/tree/1.2.0"
},
"time": "2024-02-19T18:29:05+00:00"
},
{
"name": "php-http/discovery",
"version": "1.20.0",

View File

@@ -6,11 +6,3 @@ controllersIndex:
defaults:
schemes: [ https ]
controllersSearch:
resource:
path: ../src/Search/Framework/Controller
namespace: App\Search\Framework\Controller
type: attribute
defaults:
schemes: [ https ]

View File

@@ -2,7 +2,9 @@
namespace App\Controller;
use App\Search\Action\Handler\GetMediaInfoHandler;
use App\Search\Action\Handler\SearchHandler;
use App\Search\Action\Input\GetMediaInfoInput;
use App\Search\Action\Input\SearchInput;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@@ -12,17 +14,30 @@ final class SearchController extends AbstractController
{
public function __construct(
private SearchHandler $searchHandler,
private GetMediaInfoHandler $getMediaInfoHandler,
) {}
#[Route('/search', name: 'app_search', methods: ['GET'])]
public function index(
public function search(
SearchInput $searchInput,
): Response
{
): Response {
$results = $this->searchHandler->handle($searchInput->toCommand());
return $this->render('search/results.html.twig', [
'results' => $results,
]);
}
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
public function result(
GetMediaInfoInput $getDownloadOptionsInput,
): Response {
$result = $this->getMediaInfoHandler->handle(
$getDownloadOptionsInput->toCommand()
);
return $this->render('search/result.html.twig', [
'result' => $result,
]);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Download\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class GetDownloadOptionsCommand implements CommandInterface
{
/** @implements CommandInterface<GetDownloadOptionsCommand> */
public function __construct(
public string $tmdbId,
public string $mediaType,
) {}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Download\Action\Handler;
use App\Tmdb\Tmdb;
use App\Torrentio\Torrentio;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
class GetDownloadOptionsHandler implements HandlerInterface
{
public function __construct(
private readonly Tmdb $tmdb,
private readonly Torrentio $torrentio,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$media = $this->tmdb->mediaDetails($command->tmdbId, $command->mediaType);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Download\Action\Input;
use App\Download\Action\Command\GetDownloadOptionsCommand;
use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
class GetDownloadOptionsInput implements InputInterface
{
public function __construct(
#[SourceRoute('tmdbId')]
public string $tmdbId,
#[SourceRoute('mediaType')]
public string $mediaType,
) {}
public function toCommand(): CommandInterface
{
return new GetDownloadOptionsCommand($this->tmdbId, $this->mediaType);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Download\Action\Result;
use App\Tmdb\TmdbResult;
class GetDownloadOptionsResult
{
public function __construct(
public TmdbResult $media,
) {}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Search\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class GetMediaInfoCommand implements CommandInterface
{
/** @implements CommandInterface<GetMediaInfoCommand> */
public function __construct(
public string $tmdbId,
public string $mediaType,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Search\Action\Handler;
use App\Search\Action\Result\GetMediaInfoResult;
use App\Tmdb\Tmdb;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
class GetMediaInfoHandler implements HandlerInterface
{
public function __construct(
private readonly Tmdb $tmdb,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$media = $this->tmdb->mediaDetails($command->tmdbId, $command->mediaType);
return new GetMediaInfoResult($media);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Search\Action\Input;
use App\Download\Action\Command\GetDownloadOptionsCommand;
use App\Search\Action\Command\GetMediaInfoCommand;
use OneToMany\RichBundle\Attribute\SourceRoute;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
class GetMediaInfoInput implements InputInterface
{
public function __construct(
#[SourceRoute('tmdbId')]
public string $tmdbId,
#[SourceRoute('mediaType')]
public string $mediaType,
) {}
public function toCommand(): CommandInterface
{
return new GetMediaInfoCommand($this->tmdbId, $this->mediaType);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Search\Action\Result;
use App\Tmdb\TmdbResult;
use OneToMany\RichBundle\Contract\ResultInterface;
class GetMediaInfoResult implements ResultInterface
{
/** @implements ResultInterface<GetMediaInfoResult> */
public function __construct(
public TmdbResult $media,
) {}
}

View File

@@ -182,10 +182,10 @@ class Tmdb
function parseTvShow(array $data, string $posterBasePath): TmdbResult {
return new TmdbResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
tmdbId: $data['id'],
title: $data['name'],
poster: $posterBasePath . $data['poster_path'],
excerpt: $data['overview'],
description: $data['overview'],
year: (new \DateTime($data['first_air_date']))->format('Y'),
mediaType: "tvshows",
episodes: $data['episodes'],
@@ -195,10 +195,10 @@ class Tmdb
function parseMovie(array $data, string $posterBasePath): TmdbResult {
return new TmdbResult(
imdbId: $data['external_ids']['imdb_id'],
tvdbId: $data['id'],
tmdbId: $data['id'],
title: $data['title'],
poster: $posterBasePath . $data['poster_path'],
excerpt: $data['overview'],
description: $data['overview'],
year: (new \DateTime($data['release_date']))->format('Y'),
mediaType: "movies",
);
@@ -215,6 +215,7 @@ class Tmdb
$mediaType = $result instanceof Movie ? MediaType::Movie->value : MediaType::TvShow->value;
$tmdbResult = new TmdbResult();
$tmdbResult->mediaType = $mediaType;
$tmdbResult->tmdbId = $result->getId();
$tmdbResult->imdbId = $this->getImdbId($result->getId(), $mediaType);
$tmdbResult->title = $this->getTitle($result, $mediaType);
$tmdbResult->poster = self::POSTER_IMG_PATH . $result->getPosterImage();

View File

@@ -6,7 +6,7 @@ class TmdbResult
{
public function __construct(
public ?string $imdbId = "",
public ?string $tvdbId = "",
public ?string $tmdbId = "",
public ?string $title = "",
public ?string $poster = "",
public ?string $description = "",

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;
}
}

260
src/Util/CountryCodes.php Normal file
View File

@@ -0,0 +1,260 @@
<?php
namespace App\Util;
class CountryCodes
{
static $countries = [
'AF' => 'Afghanistan',
'AX' => 'Aland Islands',
'AL' => 'Albania',
'DZ' => 'Algeria',
'AS' => 'American Samoa',
'AD' => 'Andorra',
'AO' => 'Angola',
'AI' => 'Anguilla',
'AQ' => 'Antarctica',
'AG' => 'Antigua and Barbuda',
'AR' => 'Argentina',
'AM' => 'Armenia',
'AW' => 'Aruba',
'AU' => 'Australia',
'AT' => 'Austria',
'AZ' => 'Azerbaijan',
'BS' => 'Bahamas the',
'BH' => 'Bahrain',
'BD' => 'Bangladesh',
'BB' => 'Barbados',
'BY' => 'Belarus',
'BE' => 'Belgium',
'BZ' => 'Belize',
'BJ' => 'Benin',
'BM' => 'Bermuda',
'BT' => 'Bhutan',
'BO' => 'Bolivia',
'BA' => 'Bosnia and Herzegovina',
'BW' => 'Botswana',
'BV' => 'Bouvet Island (Bouvetoya)',
'BR' => 'Brazil',
'IO' => 'British Indian Ocean Territory (Chagos Archipelago)',
'VG' => 'British Virgin Islands',
'BN' => 'Brunei Darussalam',
'BG' => 'Bulgaria',
'BF' => 'Burkina Faso',
'BI' => 'Burundi',
'KH' => 'Cambodia',
'CM' => 'Cameroon',
'CA' => 'Canada',
'CV' => 'Cape Verde',
'KY' => 'Cayman Islands',
'CF' => 'Central African Republic',
'TD' => 'Chad',
'CL' => 'Chile',
'CN' => 'China',
'CX' => 'Christmas Island',
'CC' => 'Cocos (Keeling) Islands',
'CO' => 'Colombia',
'KM' => 'Comoros the',
'CD' => 'Congo',
'CG' => 'Congo the',
'CK' => 'Cook Islands',
'CR' => 'Costa Rica',
'CI' => 'Cote d\'Ivoire',
'HR' => 'Croatia',
'CU' => 'Cuba',
'CY' => 'Cyprus',
'CZ' => 'Czech Republic',
'DK' => 'Denmark',
'DJ' => 'Djibouti',
'DM' => 'Dominica',
'DO' => 'Dominican Republic',
'EC' => 'Ecuador',
'EG' => 'Egypt',
'SV' => 'El Salvador',
'GQ' => 'Equatorial Guinea',
'ER' => 'Eritrea',
'EE' => 'Estonia',
'ET' => 'Ethiopia',
'FO' => 'Faroe Islands',
'FK' => 'Falkland Islands (Malvinas)',
'FJ' => 'Fiji the Fiji Islands',
'FI' => 'Finland',
'FR' => 'France, French Republic',
'GF' => 'French Guiana',
'PF' => 'French Polynesia',
'TF' => 'French Southern Territories',
'GA' => 'Gabon',
'GM' => 'Gambia the',
'GE' => 'Georgia',
'DE' => 'Germany',
'GH' => 'Ghana',
'GI' => 'Gibraltar',
'GR' => 'Greece',
'GL' => 'Greenland',
'GD' => 'Grenada',
'GP' => 'Guadeloupe',
'GU' => 'Guam',
'GT' => 'Guatemala',
'GG' => 'Guernsey',
'GN' => 'Guinea',
'GW' => 'Guinea-Bissau',
'GY' => 'Guyana',
'HT' => 'Haiti',
'HM' => 'Heard Island and McDonald Islands',
'VA' => 'Holy See (Vatican City State)',
'HN' => 'Honduras',
'HK' => 'Hong Kong',
'HU' => 'Hungary',
'IS' => 'Iceland',
'IN' => 'India',
'ID' => 'Indonesia',
'IR' => 'Iran',
'IQ' => 'Iraq',
'IE' => 'Ireland',
'IM' => 'Isle of Man',
'IL' => 'Israel',
'IT' => 'Italy',
'JM' => 'Jamaica',
'JP' => 'Japan',
'JE' => 'Jersey',
'JO' => 'Jordan',
'KZ' => 'Kazakhstan',
'KE' => 'Kenya',
'KI' => 'Kiribati',
'KP' => 'Korea',
'KR' => 'Korea',
'KW' => 'Kuwait',
'KG' => 'Kyrgyz Republic',
'LA' => 'Lao',
'LV' => 'Latvia',
'LB' => 'Lebanon',
'LS' => 'Lesotho',
'LR' => 'Liberia',
'LY' => 'Libyan Arab Jamahiriya',
'LI' => 'Liechtenstein',
'LT' => 'Lithuania',
'LU' => 'Luxembourg',
'MO' => 'Macao',
'MK' => 'Macedonia',
'MG' => 'Madagascar',
'MW' => 'Malawi',
'MY' => 'Malaysia',
'MV' => 'Maldives',
'ML' => 'Mali',
'MT' => 'Malta',
'MH' => 'Marshall Islands',
'MQ' => 'Martinique',
'MR' => 'Mauritania',
'MU' => 'Mauritius',
'YT' => 'Mayotte',
'MX' => 'Mexico',
'FM' => 'Micronesia',
'MD' => 'Moldova',
'MC' => 'Monaco',
'MN' => 'Mongolia',
'ME' => 'Montenegro',
'MS' => 'Montserrat',
'MA' => 'Morocco',
'MZ' => 'Mozambique',
'MM' => 'Myanmar',
'NA' => 'Namibia',
'NR' => 'Nauru',
'NP' => 'Nepal',
'AN' => 'Netherlands Antilles',
'NL' => 'Netherlands the',
'NC' => 'New Caledonia',
'NZ' => 'New Zealand',
'NI' => 'Nicaragua',
'NE' => 'Niger',
'NG' => 'Nigeria',
'NU' => 'Niue',
'NF' => 'Norfolk Island',
'MP' => 'Northern Mariana Islands',
'NO' => 'Norway',
'OM' => 'Oman',
'PK' => 'Pakistan',
'PW' => 'Palau',
'PS' => 'Palestinian Territory',
'PA' => 'Panama',
'PG' => 'Papua New Guinea',
'PY' => 'Paraguay',
'PE' => 'Peru',
'PH' => 'Philippines',
'PN' => 'Pitcairn Islands',
'PL' => 'Poland',
'PT' => 'Portugal, Portuguese Republic',
'PR' => 'Puerto Rico',
'QA' => 'Qatar',
'RE' => 'Reunion',
'RO' => 'Romania',
'RU' => 'Russian Federation',
'RW' => 'Rwanda',
'BL' => 'Saint Barthelemy',
'SH' => 'Saint Helena',
'KN' => 'Saint Kitts and Nevis',
'LC' => 'Saint Lucia',
'MF' => 'Saint Martin',
'PM' => 'Saint Pierre and Miquelon',
'VC' => 'Saint Vincent and the Grenadines',
'WS' => 'Samoa',
'SM' => 'San Marino',
'ST' => 'Sao Tome and Principe',
'SA' => 'Saudi Arabia',
'SN' => 'Senegal',
'RS' => 'Serbia',
'SC' => 'Seychelles',
'SL' => 'Sierra Leone',
'SG' => 'Singapore',
'SK' => 'Slovakia (Slovak Republic)',
'SI' => 'Slovenia',
'SB' => 'Solomon Islands',
'SO' => 'Somalia, Somali Republic',
'ZA' => 'South Africa',
'GS' => 'South Georgia and the South Sandwich Islands',
'ES' => 'Spain',
'LK' => 'Sri Lanka',
'SD' => 'Sudan',
'SR' => 'Suriname',
'SJ' => 'Svalbard & Jan Mayen Islands',
'SZ' => 'Swaziland',
'SE' => 'Sweden',
'CH' => 'Switzerland, Swiss Confederation',
'SY' => 'Syrian Arab Republic',
'TW' => 'Taiwan',
'TJ' => 'Tajikistan',
'TZ' => 'Tanzania',
'TH' => 'Thailand',
'TL' => 'Timor-Leste',
'TG' => 'Togo',
'TK' => 'Tokelau',
'TO' => 'Tonga',
'TT' => 'Trinidad and Tobago',
'TN' => 'Tunisia',
'TR' => 'Turkey',
'TM' => 'Turkmenistan',
'TC' => 'Turks and Caicos Islands',
'TV' => 'Tuvalu',
'UG' => 'Uganda',
'UA' => 'Ukraine',
'AE' => 'United Arab Emirates',
'GB' => 'United Kingdom',
'US' => 'United States of America',
'UM' => 'United States Minor Outlying Islands',
'VI' => 'United States Virgin Islands',
'UY' => 'Uruguay, Eastern Republic of',
'UZ' => 'Uzbekistan',
'VU' => 'Vanuatu',
'VE' => 'Venezuela',
'VN' => 'Vietnam',
'WF' => 'Wallis and Futuna',
'EH' => 'Western Sahara',
'YE' => 'Yemen',
'ZM' => 'Zambia',
'ZW' => 'Zimbabwe'
];
public static function convertFromAbbr(string $abbr): ?string
{
return self::$countries[$abbr] ?? null;
}
}

View File

@@ -15,7 +15,7 @@
<body class="bg-neutral-700 flex flex-col">
<twig:Header />
<div class="flex flex-row w-full">
<div class="flex flex-row mx-w-lg">
<twig:NavBar />
{% block body %}{% endblock %}
</div>

View File

@@ -10,7 +10,7 @@
</p>
</div>
<a class="h-9 rounded-md py-1 px-2 bg-green-600 text-gray-50"
href="/results/{{ mediaType }}/{{ imdbId }}/"
href="{{ url('app_search_result', {mediaType: mediaType, tmdbId: tmdbId}) }}"
>choose</a>
</div>
</div>

View File

@@ -0,0 +1,14 @@
{% extends 'base.html.twig' %}
{% block title %}Search Results &mdash - Torsearch{% endblock %}
{% block body %}
<div class="p-4 flex flex-col grow gap-4">
<h2 class="mb-2 text-3xl font-bold text-gray-50">Search Results</h2>
<div class="flex flex-row w-full gap-2">
<twig:Card title="{{ result.media.title }}" contentClass="flex flex-col gap-4 justify-between w-full">
</twig:Card>
</div>
</div>
{% endblock %}

View File

@@ -17,6 +17,7 @@
description="{{ result.description }}"
poster="{{ result.poster }}"
imdbId="{{ result.imdbId }}"
tmdbId="{{ result.tmdbId }}"
/>
</li>
</ul>