feat: new Discover section shows watch providers for results
This commit is contained in:
54
assets/controllers/discover_media_results_controller.js
Normal file
54
assets/controllers/discover_media_results_controller.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||||
|
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ['poster', 'moreBtn', 'moreLink']
|
||||||
|
|
||||||
|
moreResultsClicks = 0;
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Called once when the controller is first instantiated (per element)
|
||||||
|
|
||||||
|
// Here you can initialize variables, create scoped callables for event
|
||||||
|
// listeners, instantiate external libraries, etc.
|
||||||
|
// this._fooBar = this.fooBar.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Called every time the controller is connected to the DOM
|
||||||
|
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
|
||||||
|
|
||||||
|
// Here you can add event listeners on the element or target elements,
|
||||||
|
// add or remove classes, attributes, dispatch custom events, etc.
|
||||||
|
// this.fooTarget.addEventListener('click', this._fooBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom controller actions here
|
||||||
|
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Called anytime its element is disconnected from the DOM
|
||||||
|
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||||
|
|
||||||
|
// Here you should remove all event listeners added in "connect()"
|
||||||
|
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
moreResults() {
|
||||||
|
const elems = this.posterTargets.filter(poster => poster.classList.contains('hidden'));
|
||||||
|
if (this.moreResultsClicks <= 2) {
|
||||||
|
elems.slice(0, 6).forEach(poster => poster.classList.remove('hidden'));
|
||||||
|
this.moreResultsClicks++;
|
||||||
|
|
||||||
|
if (this.moreResultsClicks === 2) {
|
||||||
|
this.moreBtnTarget.classList.add('hidden');
|
||||||
|
this.moreLinkTarget.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,6 @@ return [
|
|||||||
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||||
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
|
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
|
||||||
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
|
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
|
||||||
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
|
Sentry\SentryBundle\SentryBundle::class => ['prod' => true, 'dev' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
when@prod:
|
sentry:
|
||||||
sentry:
|
register_error_listener: true # Disables the ErrorListener to avoid duplicated log in sentry
|
||||||
register_error_listener: true # Disables the ErrorListener to avoid duplicated log in sentry
|
register_error_handler: true # Disables the ErrorListener, ExceptionListener and FatalErrorListener integrations of the base PHP SDK
|
||||||
register_error_handler: true # Disables the ErrorListener, ExceptionListener and FatalErrorListener integrations of the base PHP SDK
|
|
||||||
|
|
||||||
options:
|
options:
|
||||||
release: 'torsearch@%app.version%'
|
release: 'torsearch@%app.version%'
|
||||||
enable_logs: true
|
enable_logs: true
|
||||||
traces_sample_rate: 1
|
traces_sample_rate: 1
|
||||||
profiles_sample_rate: 1
|
profiles_sample_rate: 1
|
||||||
attach_stacktrace: true
|
attach_stacktrace: true
|
||||||
|
|
||||||
tracing:
|
tracing:
|
||||||
|
enabled: true
|
||||||
|
dbal: # DB queries
|
||||||
|
enabled: true
|
||||||
|
cache: # cache pools
|
||||||
|
enabled: true
|
||||||
|
twig: # templating engine
|
||||||
enabled: true
|
enabled: true
|
||||||
dbal: # DB queries
|
|
||||||
enabled: true
|
|
||||||
cache: # cache pools
|
|
||||||
enabled: true
|
|
||||||
twig: # templating engine
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# (Optionally) Configure the breadcrumb handler as a service (needed for the breadcrumb Monolog handler)
|
# (Optionally) Configure the breadcrumb handler as a service (needed for the breadcrumb Monolog handler)
|
||||||
Sentry\Monolog\BreadcrumbHandler:
|
Sentry\Monolog\BreadcrumbHandler:
|
||||||
arguments:
|
arguments:
|
||||||
- '@Sentry\State\HubInterface'
|
- '@Sentry\State\HubInterface'
|
||||||
- !php/const Monolog\Logger::INFO # Configures the level of messages to capture as breadcrumbs
|
- !php/const Monolog\Logger::INFO # Configures the level of messages to capture as breadcrumbs
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
# (Optionally) Register the breadcrumb handler as a Monolog handler
|
# (Optionally) Register the breadcrumb handler as a Monolog handler
|
||||||
sentry_breadcrumbs:
|
sentry_breadcrumbs:
|
||||||
type: service
|
type: service
|
||||||
name: sentry_breadcrumbs
|
name: sentry_breadcrumbs
|
||||||
id: Sentry\Monolog\BreadcrumbHandler
|
id: Sentry\Monolog\BreadcrumbHandler
|
||||||
# Register the handler as a Monolog handler to capture messages as events
|
# Register the handler as a Monolog handler to capture messages as events
|
||||||
sentry:
|
sentry:
|
||||||
type: sentry
|
type: sentry
|
||||||
level: !php/const Monolog\Logger::ERROR # Configures the level of messages to capture as events
|
level: !php/const Monolog\Logger::ERROR # Configures the level of messages to capture as events
|
||||||
hub_id: Sentry\State\HubInterface
|
hub_id: Sentry\State\HubInterface
|
||||||
fill_extra_context: true # Enables sending monolog context to Sentry
|
fill_extra_context: true # Enables sending monolog context to Sentry
|
||||||
process_psr_3_messages: false # Disables the resolution of PSR-3 placeholders in reported messages
|
process_psr_3_messages: false # Disables the resolution of PSR-3 placeholders in reported messages
|
||||||
@@ -6,6 +6,14 @@ controllersBase:
|
|||||||
defaults:
|
defaults:
|
||||||
schemes: [ 'https' ]
|
schemes: [ 'https' ]
|
||||||
|
|
||||||
|
controllersDiscover:
|
||||||
|
resource:
|
||||||
|
path: ../src/Discover/Framework/Controller/
|
||||||
|
namespace: App\Discover\Framework\Controller
|
||||||
|
type: attribute
|
||||||
|
defaults:
|
||||||
|
schemes: [ 'https' ]
|
||||||
|
|
||||||
controllersEventLog:
|
controllersEventLog:
|
||||||
resource:
|
resource:
|
||||||
path: ../src/EventLog/Framework/Controller/
|
path: ../src/EventLog/Framework/Controller/
|
||||||
|
|||||||
141
src/Discover/Framework/Controller/WebController.php
Normal file
141
src/Discover/Framework/Controller/WebController.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Discover\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbMovieGenre;
|
||||||
|
use App\Tmdb\TmdbTvShowGenre;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/discover')]
|
||||||
|
class WebController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/', name: 'app.discover')]
|
||||||
|
public function index(TmdbClient $tmdb)
|
||||||
|
{
|
||||||
|
$movies = $tmdb->popularMovies(18);
|
||||||
|
$tvshows = $tmdb->popularTvShows(18);
|
||||||
|
|
||||||
|
return $this->render('discover/index.html.twig', [
|
||||||
|
'movies' => $movies,
|
||||||
|
'shows' => $tvshows,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{mediaType}/{genreId?}', name: 'app.discover.browse')]
|
||||||
|
public function browse(string $mediaType, ?string $genreId, TmdbClient $tmdb)
|
||||||
|
{
|
||||||
|
if (null === $genreId) {
|
||||||
|
if (MediaType::tryFrom($mediaType) === null) {
|
||||||
|
return new Response(status: 404);
|
||||||
|
}
|
||||||
|
return $this->render('discover/browse.html.twig', [
|
||||||
|
'genres' => self::getGenres($mediaType),
|
||||||
|
'media_type' => $mediaType,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MediaType::tryFrom($mediaType) === null) {
|
||||||
|
return new Response(status: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TmdbMovieGenre::tryFrom($genreId) === null &&
|
||||||
|
TmdbTvShowGenre::tryFrom($genreId) === null
|
||||||
|
) {
|
||||||
|
return new Response(status: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = match ($mediaType) {
|
||||||
|
MediaType::Movie->value => $tmdb->discoverMovies(
|
||||||
|
[TmdbMovieGenre::from($genreId)->value]
|
||||||
|
),
|
||||||
|
MediaType::TvShow->value => $tmdb->discoverTvShows(
|
||||||
|
[TmdbTvShowGenre::from($genreId)->value]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this->render('discover/browse_genre.html.twig', [
|
||||||
|
'media' => $results,
|
||||||
|
'genre' => TmdbMovieGenre::from($genreId)->name,
|
||||||
|
'genre_id' => $genreId,
|
||||||
|
'media_type' => $mediaType,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{mediaType}/{genreId}', name: 'app.discover.browse_genre')]
|
||||||
|
public function browseGenre(string $mediaType, string $genreId, TmdbClient $tmdb)
|
||||||
|
{
|
||||||
|
if (MediaType::tryFrom($mediaType) === null) {
|
||||||
|
return new Response(status: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TmdbMovieGenre::tryFrom($genreId) === null &&
|
||||||
|
TmdbTvShowGenre::tryFrom($genreId) === null
|
||||||
|
) {
|
||||||
|
return new Response(status: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = match ($mediaType) {
|
||||||
|
MediaType::Movie->value => $tmdb->discoverMovies(
|
||||||
|
[TmdbMovieGenre::from($genreId)->value]
|
||||||
|
),
|
||||||
|
MediaType::TvShow->value => $tmdb->discoverTvShows(
|
||||||
|
[TmdbTvShowGenre::from($genreId)->value]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this->render('discover/browse.html.twig', [
|
||||||
|
'media' => $results,
|
||||||
|
'media_type' => $mediaType,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getGenres(string $mediaType): array
|
||||||
|
{
|
||||||
|
return match ($mediaType) {
|
||||||
|
MediaType::Movie->value => [
|
||||||
|
'Action' => TmdbMovieGenre::Action->value,
|
||||||
|
'Adventure' => TmdbMovieGenre::Adventure->value,
|
||||||
|
'Animation' => TmdbMovieGenre::Animation->value,
|
||||||
|
'Comedy' => TmdbMovieGenre::Comedy->value,
|
||||||
|
'Crime' => TmdbMovieGenre::Crime->value,
|
||||||
|
'Documentary' => TmdbMovieGenre::Documentary->value,
|
||||||
|
'Drama' => TmdbMovieGenre::Drama->value,
|
||||||
|
'Family' => TmdbMovieGenre::Family->value,
|
||||||
|
'Fantasy' => TmdbMovieGenre::Fantasy->value,
|
||||||
|
'History' => TmdbMovieGenre::History->value,
|
||||||
|
'Horror' => TmdbMovieGenre::Horror->value,
|
||||||
|
'Music' => TmdbMovieGenre::Music->value,
|
||||||
|
'Mystery' => TmdbMovieGenre::Mystery->value,
|
||||||
|
'Romance' => TmdbMovieGenre::Romance->value,
|
||||||
|
'Science Fiction' => TmdbMovieGenre::ScienceFiction->value,
|
||||||
|
'TV Movie' => TmdbMovieGenre::TvMovie->value,
|
||||||
|
'Thriller' => TmdbMovieGenre::Thriller->value,
|
||||||
|
'War' => TmdbMovieGenre::War->value,
|
||||||
|
'Western' => TmdbMovieGenre::Western->value,
|
||||||
|
],
|
||||||
|
MediaType::TvShow->value => [
|
||||||
|
'Action & Adventure' => TmdbTvShowGenre::ActionAndAdventure->value,
|
||||||
|
'Animation' => TmdbTvShowGenre::Animation->value,
|
||||||
|
'Comedy' => TmdbTvShowGenre::Comedy->value,
|
||||||
|
'Crime' => TmdbTvShowGenre::Crime->value,
|
||||||
|
'Documentary' => TmdbTvShowGenre::Documentary->value,
|
||||||
|
'Drama' => TmdbTvShowGenre::Drama->value,
|
||||||
|
'Family' => TmdbTvShowGenre::Family->value,
|
||||||
|
'Kids' => TmdbTvShowGenre::Kids->value,
|
||||||
|
'Mystery' => TmdbTvShowGenre::Mystery->value,
|
||||||
|
'News' => TmdbTvShowGenre::News->value,
|
||||||
|
'Reality' => TmdbTvShowGenre::Reality->value,
|
||||||
|
'Sci-Fi & Fantasy' => TmdbTvShowGenre::SciFiAndFantasy->value,
|
||||||
|
'Soap' => TmdbTvShowGenre::Soap->value,
|
||||||
|
'Talk' => TmdbTvShowGenre::Talk->value,
|
||||||
|
'War & Politics' => TmdbTvShowGenre::WarAndPolitics->value,
|
||||||
|
'Western' => TmdbTvShowGenre::Western->value,
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Tmdb/Dto/WatchProviderDto.php
Normal file
22
src/Tmdb/Dto/WatchProviderDto.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class WatchProviderDto
|
||||||
|
{
|
||||||
|
const BASE_LOGO_PATH = 'https://image.tmdb.org/t/p/w185';
|
||||||
|
|
||||||
|
#[SerializedPath('[provider_id]')]
|
||||||
|
public int $id;
|
||||||
|
#[SerializedPath('[display_priority]')]
|
||||||
|
public int $displayPriority;
|
||||||
|
#[SerializedPath('[provider_name]')]
|
||||||
|
public string $name;
|
||||||
|
#[SerializedPath('[logo_path]')]
|
||||||
|
public string $logo {
|
||||||
|
set(string $value) => self::BASE_LOGO_PATH . $value;
|
||||||
|
}
|
||||||
|
public string $url;
|
||||||
|
}
|
||||||
@@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Tmdb\Framework\Controller;
|
namespace App\Tmdb\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Util\ImdbMatcher;
|
use App\Base\Util\ImdbMatcher;
|
||||||
|
use App\Library\Action\Result\LibrarySearchResult;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbMovieGenre;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
|
use App\Tmdb\TmdbTvShowGenre;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\UX\Turbo\TurboBundle;
|
||||||
|
|
||||||
class ApiController extends AbstractController
|
class ApiController extends AbstractController
|
||||||
{
|
{
|
||||||
@@ -47,4 +52,50 @@ class ApiController extends AbstractController
|
|||||||
'results' => $results,
|
'results' => $results,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/tmdb/watch-providers/{mediaType}/{tmdbId}', name: 'api.tmdb.watch_providers', methods: ['GET'])]
|
||||||
|
public function watchProviders(string $mediaType, string $tmdbId, Request $request, TmdbClient $tmdb)
|
||||||
|
{
|
||||||
|
$result = $tmdb->watchProviders($tmdbId, $mediaType);
|
||||||
|
if ($request->headers->get('Turbo-Frame')) {
|
||||||
|
return $this->sendFragmentResponse(['providers' => $result], $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/tmdb/genre/{mediaType}/{genreId}', name: 'api.tmdb.genre', methods: ['GET'])]
|
||||||
|
public function genreResults(string $mediaType, string $genreId, Request $request, TmdbClient $tmdb)
|
||||||
|
{
|
||||||
|
$genre = TmdbMovieGenre::from($genreId);
|
||||||
|
$results['media_type'] = $mediaType;
|
||||||
|
$results['genre'] = $genre->name;
|
||||||
|
$results['genre_id'] = $genre->value;
|
||||||
|
$results['media'] = match($mediaType) {
|
||||||
|
MediaType::Movie->value => $tmdb->discoverMovies(
|
||||||
|
[TmdbMovieGenre::from($genre->value)],
|
||||||
|
),
|
||||||
|
MediaType::TvShow->value => $tmdb->discoverTvShows(
|
||||||
|
[TmdbTvShowGenre::from($genreId)]
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ($request->headers->get('Turbo-Frame')) {
|
||||||
|
return $this->sendFragmentResponse(['result' => $results], $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendFragmentResponse(mixed $result, Request $request): Response
|
||||||
|
{
|
||||||
|
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||||
|
return $this->renderBlock(
|
||||||
|
'discover/fragments.html.twig',
|
||||||
|
$request->query->get('block'),
|
||||||
|
[
|
||||||
|
'result' => $result,
|
||||||
|
'target' => $request->query->get('target')
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Base\Enum\MediaType;
|
|||||||
use App\Tmdb\Dto\CastMemberDto;
|
use App\Tmdb\Dto\CastMemberDto;
|
||||||
use App\Tmdb\Dto\CrewMemberDto;
|
use App\Tmdb\Dto\CrewMemberDto;
|
||||||
use App\Tmdb\Dto\GenreDto;
|
use App\Tmdb\Dto\GenreDto;
|
||||||
|
use App\Tmdb\Dto\WatchProviderDto;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
@@ -28,6 +29,7 @@ class TmdbResultDenormalizer implements DenormalizerInterface
|
|||||||
$result->directors = $this->getDirectors($data);
|
$result->directors = $this->getDirectors($data);
|
||||||
$result->producers = $this->getProducers($data);
|
$result->producers = $this->getProducers($data);
|
||||||
$result->creators = $this->getCreators($data);
|
$result->creators = $this->getCreators($data);
|
||||||
|
$result->watchProviders = $this->getWatchProviders($data);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,17 @@ class TmdbResultDenormalizer implements DenormalizerInterface
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getWatchProviders(array $data): ?array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('watch/providers', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// ToDo: Make region configurable
|
||||||
|
return Map::from($data['watch/providers']['results']['US']['flatrate'])
|
||||||
|
->map(fn($item) => $this->normalizer->denormalize($item, WatchProviderDto::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function supportsDenormalization(
|
public function supportsDenormalization(
|
||||||
mixed $data,
|
mixed $data,
|
||||||
string $type,
|
string $type,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ use Aimeos\Map;
|
|||||||
use App\Base\Enum\MediaType;
|
use App\Base\Enum\MediaType;
|
||||||
use App\Base\Util\ImdbMatcher;
|
use App\Base\Util\ImdbMatcher;
|
||||||
use App\Tmdb\Dto\TmdbEpisodeDto;
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
|
use App\Tmdb\Dto\WatchProviderDto;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use Tmdb\Api\Find;
|
use Tmdb\Api\Find;
|
||||||
use Tmdb\Client;
|
use Tmdb\Client;
|
||||||
@@ -31,7 +33,7 @@ use Tmdb\Token\Api\BearerToken;
|
|||||||
class TmdbClient
|
class TmdbClient
|
||||||
{
|
{
|
||||||
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
||||||
const APPEND_TO_RESPONSE = 'external_ids,credits,watch/providers';
|
const APPEND_TO_RESPONSE = 'external_ids,credits';
|
||||||
|
|
||||||
protected Client $client;
|
protected Client $client;
|
||||||
protected MovieRepository $movieRepository;
|
protected MovieRepository $movieRepository;
|
||||||
@@ -139,6 +141,50 @@ class TmdbClient
|
|||||||
return $this->parseListOfResults($results);
|
return $this->parseListOfResults($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function discoverMovies(array $genres = [], int $page = 1, int $pageSize = 24, array $params = []): TmdbResult|Map
|
||||||
|
{
|
||||||
|
if (!empty($genres) && $genres[0] instanceof TmdbMovieGenre) {
|
||||||
|
$genres = array_map(fn ($genre) => $genre->value, $genres);
|
||||||
|
}
|
||||||
|
$results = $this->discoverRepository->getApi()->discoverMovies([
|
||||||
|
'page' => $page,
|
||||||
|
'page_size' => $pageSize,
|
||||||
|
'with_genres' => implode(',', $genres),
|
||||||
|
'with_original_language' => $this->originalLanguage,
|
||||||
|
'append_to_response' => static::APPEND_TO_RESPONSE,
|
||||||
|
]);
|
||||||
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
|
$result['media_type'] = MediaType::Movie->value;
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$results,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discoverTvshows(array $genres = [], int $page = 1, int $pageSize = 24, array $params = []): TmdbResult|Map
|
||||||
|
{
|
||||||
|
if (!empty($genres) && $genres[0] instanceof TmdbTvShowGenre) {
|
||||||
|
$genres = array_map(fn ($genre) => $genre->value, $genres);
|
||||||
|
}
|
||||||
|
$results = $this->discoverRepository->getApi()->discoverTv([
|
||||||
|
'page' => $page,
|
||||||
|
'page_size' => $pageSize,
|
||||||
|
'with_genres' => implode(',', $genres),
|
||||||
|
'with_original_language' => $this->originalLanguage,
|
||||||
|
'append_to_response' => static::APPEND_TO_RESPONSE,
|
||||||
|
]);
|
||||||
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
|
$result['media_type'] = MediaType::TvShow->value;
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->parseListOfResults(
|
||||||
|
$results,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function movieDetails(string $imdbId): ?TmdbResult
|
public function movieDetails(string $imdbId): ?TmdbResult
|
||||||
{
|
{
|
||||||
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
@@ -201,6 +247,14 @@ class TmdbClient
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function watchProviders(string $tmdbId, string $mediaType): Map
|
||||||
|
{
|
||||||
|
$results = $this->repos[$mediaType]->getApi()->getWatchProviders($tmdbId)['results']['US']['flatrate'];
|
||||||
|
return Map::from($results)->map(function ($result) {
|
||||||
|
return $this->serializer->denormalize($result, WatchProviderDto::class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function popularMovies(int $resultCount = 6): Map
|
public function popularMovies(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
$results = $this->discoverRepository->getApi()->discoverMovies([
|
$results = $this->discoverRepository->getApi()->discoverMovies([
|
||||||
|
|||||||
26
src/Tmdb/TmdbMovieGenre.php
Normal file
26
src/Tmdb/TmdbMovieGenre.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
enum TmdbMovieGenre: int
|
||||||
|
{
|
||||||
|
case Action = 28;
|
||||||
|
case Adventure = 12;
|
||||||
|
case Animation = 16;
|
||||||
|
case Comedy = 35;
|
||||||
|
case Crime = 80;
|
||||||
|
case Documentary = 99;
|
||||||
|
case Drama = 18;
|
||||||
|
case Family = 10751;
|
||||||
|
case Fantasy = 14;
|
||||||
|
case History = 36;
|
||||||
|
case Horror = 27;
|
||||||
|
case Music = 10402;
|
||||||
|
case Mystery = 9648;
|
||||||
|
case Romance = 10749;
|
||||||
|
case ScienceFiction = 878;
|
||||||
|
case TvMovie = 10770;
|
||||||
|
case Thriller = 53;
|
||||||
|
case War = 10752;
|
||||||
|
case Western = 37;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use App\Tmdb\Dto\CastMemberDto;
|
|||||||
use App\Tmdb\Dto\CrewMemberDto;
|
use App\Tmdb\Dto\CrewMemberDto;
|
||||||
use App\Tmdb\Dto\GenreDto;
|
use App\Tmdb\Dto\GenreDto;
|
||||||
use App\Tmdb\Dto\TmdbEpisodeDto;
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
|
use App\Tmdb\Dto\WatchProviderDto;
|
||||||
use Symfony\Component\Serializer\Attribute\Context;
|
use Symfony\Component\Serializer\Attribute\Context;
|
||||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
@@ -57,5 +58,6 @@ class TmdbResult
|
|||||||
public ?int $runtime = null,
|
public ?int $runtime = null,
|
||||||
public ?int $numberSeasons = null,
|
public ?int $numberSeasons = null,
|
||||||
public ?int $latestSeason = null,
|
public ?int $latestSeason = null,
|
||||||
|
public ?array $watchProviders = null
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/Tmdb/TmdbTvShowGenre.php
Normal file
23
src/Tmdb/TmdbTvShowGenre.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
enum TmdbTvShowGenre: int
|
||||||
|
{
|
||||||
|
case ActionAndAdventure = 10759;
|
||||||
|
case Animation = 16;
|
||||||
|
case Comedy = 35;
|
||||||
|
case Crime = 80;
|
||||||
|
case Documentary = 99;
|
||||||
|
case Drama = 18;
|
||||||
|
case Family = 10751;
|
||||||
|
case Kids = 10762;
|
||||||
|
case Mystery = 9648;
|
||||||
|
case News = 10763;
|
||||||
|
case Reality = 10764;
|
||||||
|
case SciFiAndFantasy = 10765;
|
||||||
|
case Soap = 10766;
|
||||||
|
case Talk = 10767;
|
||||||
|
case WarAndPolitics = 10768;
|
||||||
|
case Western = 37;
|
||||||
|
}
|
||||||
18
src/Twig/Components/PosterContainer.php
Normal file
18
src/Twig/Components/PosterContainer.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Twig\Components;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||||
|
|
||||||
|
#[AsTwigComponent]
|
||||||
|
final class PosterContainer
|
||||||
|
{
|
||||||
|
public array|Map $media;
|
||||||
|
public string $mediaType;
|
||||||
|
public ?string $genreId = null;
|
||||||
|
public ?string $genre = null;
|
||||||
|
|
||||||
|
// Only show 6 results and a 'more' button
|
||||||
|
public bool $tease = true;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
{{ pwa() }}
|
{{ pwa() }}
|
||||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
<title>{% block title %}Torsearch{% endblock %}</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
|
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
|
||||||
|
|||||||
@@ -29,6 +29,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{ path('app.discover') }}"
|
||||||
|
class="block rounded-lg
|
||||||
|
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
||||||
|
px-4 py-2 text-sm font-medium text-gray-50">
|
||||||
|
Discover
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app_user_preferences') }}"
|
<a href="{{ path('app_user_preferences') }}"
|
||||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700">
|
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700">
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<div{{ attributes }}>
|
<div{{ attributes }}>
|
||||||
|
{% if image != null and image != "https://image.tmdb.org/t/p/w500" %}
|
||||||
|
<a href="{{ path('app_search_result', {
|
||||||
|
mediaType: mediaType,
|
||||||
|
imdbId: imdbId
|
||||||
|
}) }}">
|
||||||
|
<img src="{{ preload(image) }}" class="w-full rounded-md" />
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="w-full md:w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
|
<twig:ux:icon width="16" name="hugeicons:loading-01" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a href="{{ path('app_search_result', {
|
<a href="{{ path('app_search_result', {
|
||||||
mediaType: mediaType,
|
mediaType: mediaType,
|
||||||
imdbId: imdbId
|
imdbId: imdbId
|
||||||
}) }}">
|
}) }}">
|
||||||
<img src="{{ preload(image) }}" class="w-full md:w-40 rounded-md" />
|
<h3 class="mt-2 text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
||||||
</a>
|
|
||||||
<a href="{{ path('app_search_result', {
|
|
||||||
mediaType: mediaType,
|
|
||||||
imdbId: imdbId
|
|
||||||
}) }}">
|
|
||||||
<h3 class="text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
templates/components/PosterContainer.html.twig
Normal file
30
templates/components/PosterContainer.html.twig
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div{{ attributes.defaults(stimulus_controller('discover_media_results')) }} class="flex flex-col">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||||
|
{% for i in range(0, media|length - 1) %}
|
||||||
|
{% if i > 5 and tease is true %}
|
||||||
|
{% set class_list = "hidden" %}
|
||||||
|
{% else %}
|
||||||
|
{% set class_list = "" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set poster = media[i] %}
|
||||||
|
<twig:Poster data-discover-media-results-target="poster"
|
||||||
|
imdbId="{{ poster.imdbId }}"
|
||||||
|
tmdbId="{{ poster.tmdbId }}"
|
||||||
|
title="{{ poster.title }}"
|
||||||
|
description="{{ poster.description }}"
|
||||||
|
image="{{ poster.poster }}"
|
||||||
|
year="{{ poster.year }}"
|
||||||
|
mediaType="movies"
|
||||||
|
class="pb-2 w-full rounded-lg {{ class_list }}"
|
||||||
|
/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tease == true %}
|
||||||
|
<div class="inline-flex self-end text-white">
|
||||||
|
<button data-discover-media-results-target="moreBtn" data-action="click->discover-media-results#moreResults" href="#" class="underline">More</button>
|
||||||
|
<a data-discover-media-results-target="moreLink" href="{{ url('app.discover.browse', {mediaType: mediaType, page: 2, genreId: genreId}) }}" class="underline hidden">More ></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div{{ attributes }}>
|
<div{{ attributes }}>
|
||||||
<div class="p-4 flex flex-col md:flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
<div class="p-4 flex flex-col md:flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||||
{% if poster != null and poster != "https://image.tmdb.org/t/p/w500" %}
|
{% if poster != null and poster != "https://image.tmdb.org/t/p/w500" %}
|
||||||
<img class="w-full md:w-24 rounded-lg" src="{{ poster }}" />
|
<img class="w-full md:w-24 rounded-lg" src="{{ preload(poster) }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full md:w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
<div class="w-full md:w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
<twig:ux:icon width="16" name="hugeicons:loading-01" />
|
<twig:ux:icon width="16" name="hugeicons:loading-01" />
|
||||||
|
|||||||
17
templates/discover/browse.html.twig
Normal file
17
templates/discover/browse.html.twig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% block title %}Discover {{ media_type|capitalize }} — {{ parent() }}{% endblock %}
|
||||||
|
{% block h2 %}Discover {{ media_type|capitalize }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
{% for genreTitle, genreId in genres %}
|
||||||
|
<twig:Turbo:Frame id="genre_{{ media_type }}_{{ genreId }}" src="{{ path('api.tmdb.genre', {
|
||||||
|
mediaType: media_type,
|
||||||
|
genreId: genreId,
|
||||||
|
block: 'genre_results',
|
||||||
|
target: 'genre_' ~ media_type~ '_' ~ genreId
|
||||||
|
}) }}">
|
||||||
|
</twig:Turbo:Frame>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
11
templates/discover/browse_genre.html.twig
Normal file
11
templates/discover/browse_genre.html.twig
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% block title %}Discover {{ genre }} {{ media_type|capitalize }} — {{ parent() }}{% endblock %}
|
||||||
|
{% block h2 %}Discover {{ genre }} {{ media_type|capitalize }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
<twig:Card title="{{ genre }}" class="w-full">
|
||||||
|
<twig:PosterContainer tease="'false'" genreId="{{ genre_id }}" mediaType="{{ media_type }}" media="{{ media }}"></twig:PosterContainer>
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
25
templates/discover/fragments.html.twig
Normal file
25
templates/discover/fragments.html.twig
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% block watch_providers %}
|
||||||
|
{% if result.providers %}
|
||||||
|
<turbo-stream action="replace" targets="#{{ target }}">
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-row justify-start items-end gap-1 mt-2">
|
||||||
|
{% for provider in result.providers %}
|
||||||
|
<a href="#">
|
||||||
|
<img class="w-10 h-10 rounded-lg" src="{{ provider.logo }}" alt="{{ provider.name }}" title="{{ provider.name }}" />
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</turbo-stream>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block genre_results %}
|
||||||
|
<turbo-stream action="replace" targets="#{{ target }}">
|
||||||
|
<template>
|
||||||
|
<twig:Card title="{{ result.result.genre }}" class="w-full">
|
||||||
|
<twig:PosterContainer genreId="{{ result.result.genre_id }}" mediaType="{{ result.result.media_type }}" media="{{ result.result.media }}"></twig:PosterContainer>
|
||||||
|
</twig:Card>
|
||||||
|
</template>
|
||||||
|
</turbo-stream>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Discover — {{ parent() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block h2 %}Discover New Media{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
<twig:Card title="Popular Movies" class="w-full">
|
||||||
|
<twig:PosterContainer mediaType="movies" media="{{ movies }}" />
|
||||||
|
</twig:Card>
|
||||||
|
|
||||||
|
<twig:Card title="Popular Shows" class="w-full">
|
||||||
|
<twig:PosterContainer mediaType="tvshows" media="{{ shows }}" />
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -76,48 +76,57 @@
|
|||||||
{% if results.media.genres != null %}
|
{% if results.media.genres != null %}
|
||||||
<div id="genres" class="text-gray-50 my-4">
|
<div id="genres" class="text-gray-50 my-4">
|
||||||
{% for genre in results.media.genres %}
|
{% for genre in results.media.genres %}
|
||||||
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
<a href="{{ url('app.discover.browse_genre', {mediaType: results.media.mediaType, genreId: genre.id}) }}" class="px-2 py-1 border border-orange-500 rounded-full text-sm">{{ genre }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row justify-start items-end grow text-xs">
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
<span>{{ results.media.numberSeasons }}</span> season(s)
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
|
||||||
</span>
|
<span>{{ results.media.numberSeasons }}</span> season(s)
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
|
</span>
|
||||||
{{ results.media.premiereDate|date(null, 'UTC') }}
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
|
||||||
</span>
|
{{ results.media.premiereDate|date(null, 'UTC') }}
|
||||||
</div>
|
</span>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if "movies" == results.media.mediaType %}
|
{% if "movies" == results.media.mediaType %}
|
||||||
<div class="flex flex-row justify-start items-end grow text-xs">
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
<span class="results-count-number" id="movie_results_count">-</span> results
|
<span class="results-count-number" id="movie_results_count">-</span> results
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||||
title: results.media.title,
|
title: results.media.title,
|
||||||
block: 'media_exists_badge',
|
block: 'media_exists_badge',
|
||||||
target: "meb_" ~ results.media.imdbId
|
target: "meb_" ~ results.media.imdbId
|
||||||
|
}) }}">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
||||||
|
missing
|
||||||
|
</span>
|
||||||
|
</twig:Turbo:Frame>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||||
|
{{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white" title="This movie has a runtime of {{ results.media.runtime }} minutes.">
|
||||||
|
{{ results.media.runtime }} minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<twig:Turbo:Frame id="watch_providers_frame" src="{{ path('api.tmdb.watch_providers', {
|
||||||
|
mediaType: results.media.mediaType,
|
||||||
|
tmdbId: results.media.tmdbId,
|
||||||
|
block: 'watch_providers',
|
||||||
|
target: 'watch_providers_frame'
|
||||||
}) }}">
|
}) }}">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
|
||||||
missing
|
|
||||||
</span>
|
|
||||||
</twig:Turbo:Frame>
|
</twig:Turbo:Frame>
|
||||||
|
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title="Release date {{ results.media.episodeAirDate }}">
|
|
||||||
{{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white" title="This movie has a runtime of {{ results.media.runtime }} minutes.">
|
|
||||||
{{ results.media.runtime }} minutes
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user