Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21bc43bb84 |
6
.env
6
.env
@@ -61,10 +61,4 @@ NTFY_DSN=
|
|||||||
|
|
||||||
###> sentry/sentry-symfony ###
|
###> sentry/sentry-symfony ###
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
SENTRY_JS_URL=
|
|
||||||
###< sentry/sentry-symfony ###
|
###< sentry/sentry-symfony ###
|
||||||
|
|
||||||
# TMDB 'with_original_language' option
|
|
||||||
# - only include media originally
|
|
||||||
# produced in this language
|
|
||||||
TMDB_ORIGINAL_LANGUAGE=en
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
name: SonarQube Scan
|
|
||||||
jobs:
|
|
||||||
sonarqube:
|
|
||||||
name: SonarQube Trigger
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checking out
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: SonarQube Scan
|
|
||||||
uses: https://code.caldwell.digital/tools/sonarqube-action@v0.0.3
|
|
||||||
with:
|
|
||||||
host: "https://qube.caldwell.digital"
|
|
||||||
login: ${{ secrets.SONARQUBE_TOKEN }}
|
|
||||||
projectName: "torsearch"
|
|
||||||
projectBaseDir: "./src"
|
|
||||||
1
assets/bootstrap.js
vendored
1
assets/bootstrap.js
vendored
@@ -10,6 +10,7 @@ import { startStimulusApp } from '@symfony/stimulus-bundle';
|
|||||||
import Popover from '@stimulus-components/popover';
|
import Popover from '@stimulus-components/popover';
|
||||||
import Dialog from '@stimulus-components/dialog';
|
import Dialog from '@stimulus-components/dialog';
|
||||||
import Dropdown from '@stimulus-components/dropdown';
|
import Dropdown from '@stimulus-components/dropdown';
|
||||||
|
|
||||||
import 'animate.css';
|
import 'animate.css';
|
||||||
|
|
||||||
const app = startStimulusApp();
|
const app = startStimulusApp();
|
||||||
|
|||||||
40
assets/controllers/dashboard_widgets_controller.js
Normal file
40
assets/controllers/dashboard_widgets_controller.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
import {GridStack} from "../vendor/gridstack/gridstack.index.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
grid;
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.grid = GridStack.init({
|
||||||
|
column: 2,
|
||||||
|
alwaysShowResizeHandle: true,
|
||||||
|
margin: "2rem",
|
||||||
|
resizable: {
|
||||||
|
handles: 'e,se,s,sw,w'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.grid.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2zm11.5 5.175l3.5 1.556V4.269l-3.5 1.556zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 374 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773C16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318C1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593c.143-.863.698-1.723 1.464-2.383"/><path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 720 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 377 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path fill="currentColor" d="M2048 1088H250l787 787l-90 90L6 1024L947 83l90 90l-787 787h1798z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 167 B |
@@ -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, 'dev' => true],
|
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
sentry:
|
when@prod:
|
||||||
register_error_listener: true # Disables the ErrorListener to avoid duplicated log in sentry
|
sentry:
|
||||||
register_error_handler: true # Disables the ErrorListener, ExceptionListener and FatalErrorListener integrations of the base PHP SDK
|
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
|
||||||
|
|
||||||
options:
|
options:
|
||||||
release: 'torsearch@%app.version%'
|
release: '%app.version%'
|
||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
twig:
|
twig:
|
||||||
globals:
|
globals:
|
||||||
version: '%app.version%'
|
version: '%app.version%'
|
||||||
sentry_javascript_url: '%sentry.javascript_url%'
|
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
date:
|
date:
|
||||||
format: 'm/d/Y'
|
format: 'm/d/Y'
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ 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/
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ parameters:
|
|||||||
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
||||||
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
||||||
|
|
||||||
# Sentry
|
|
||||||
sentry.dsn: '%env(SENTRY_DSN)%'
|
|
||||||
sentry.javascript_url: '%env(SENTRY_JS_URL)%'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
|||||||
@@ -70,4 +70,7 @@ return [
|
|||||||
'@ungap/custom-elements' => [
|
'@ungap/custom-elements' => [
|
||||||
'version' => '1.3.0',
|
'version' => '1.3.0',
|
||||||
],
|
],
|
||||||
|
'gridstack' => [
|
||||||
|
'version' => '12.3.3',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ final class ConfigResolver
|
|||||||
|
|
||||||
#[Autowire(param: 'notification.ntfy.dsn')]
|
#[Autowire(param: 'notification.ntfy.dsn')]
|
||||||
private ?string $notificationNtfyDsn = null,
|
private ?string $notificationNtfyDsn = null,
|
||||||
|
|
||||||
#[Autowire(param: 'sentry.dsn')]
|
|
||||||
private ?string $sentryDsn = null,
|
|
||||||
|
|
||||||
#[Autowire(param: 'sentry.environment')]
|
|
||||||
private ?string $sentryEnvironment = null,
|
|
||||||
|
|
||||||
#[Autowire(param: 'sentry.javascript_url')]
|
|
||||||
private ?string $sentryJavascriptUrl = null,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function validate(): bool
|
public function validate(): bool
|
||||||
@@ -129,13 +120,4 @@ final class ConfigResolver
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSentryConfig()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'dsn' => $this->sentryDsn,
|
|
||||||
'environment' => $this->sentryEnvironment,
|
|
||||||
'javascript_url' => $this->sentryJavascriptUrl,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ final class IndexController extends AbstractController
|
|||||||
#[Route('/test')]
|
#[Route('/test')]
|
||||||
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
||||||
{
|
{
|
||||||
throw new \Exception('Test');
|
// $handler->handle(new MonitorTvShowCommand(82));
|
||||||
return $this->render('index/test.html.twig', []);
|
return $this->render('index/test.html.twig', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<?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 => [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,14 +46,14 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
$downloadCommands = [];
|
$downloadCommands = [];
|
||||||
foreach ($episodesInSeason as $episode) {
|
foreach ($episodesInSeason as $episode) {
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode->episodeNumber);
|
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode['episode_number']);
|
||||||
|
|
||||||
$results = $this->getTvShowOptionsHandler->handle(
|
$results = $this->getTvShowOptionsHandler->handle(
|
||||||
new GetTvShowOptionsCommand(
|
new GetTvShowOptionsCommand(
|
||||||
$series->tmdbId,
|
$series->tmdbId,
|
||||||
$command->imdbId,
|
$command->imdbId,
|
||||||
$command->season,
|
$command->season,
|
||||||
$episode->episodeNumber
|
$episode['episode_number']
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
if (null !== $result) {
|
if (null !== $result) {
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode->episodeNumber);
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode['episode_number']);
|
||||||
$downloadCommand = new DownloadMediaCommand(
|
$downloadCommand = new DownloadMediaCommand(
|
||||||
$result->url,
|
$result->url,
|
||||||
$series->title,
|
$series->title,
|
||||||
@@ -99,10 +99,10 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
->filter(fn ($episode) =>
|
->filter(fn ($episode) =>
|
||||||
property_exists($episode, 'episode')
|
property_exists($episode, 'episode')
|
||||||
&& property_exists($episode, 'season')
|
&& property_exists($episode, 'season')
|
||||||
&& null !== $episode->episodeNumber
|
&& null !== $episode->episode
|
||||||
&& null !== $episode->season
|
&& null !== $episode->season
|
||||||
)
|
)
|
||||||
->rekey(fn($episode) => $episode->episodeNumber);
|
->rekey(fn($episode) => $episode->episode);
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Download\Action\Handler;
|
|
||||||
|
|
||||||
use Aimeos\Map;
|
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
|
||||||
use App\Tmdb\Dto\TmdbEpisodeDto;
|
|
||||||
use App\Tmdb\TmdbClient;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
trait MonitorHandlerTrait
|
|
||||||
{
|
|
||||||
private MonitorRepository $monitorRepository;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
private TmdbClient $tmdb;
|
|
||||||
|
|
||||||
private function episodeReleasedAfterMonitorCreated(
|
|
||||||
string|DateTimeImmutable $monitorStartDate,
|
|
||||||
TmdbEpisodeDto $episodeInShow
|
|
||||||
): bool {
|
|
||||||
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
|
||||||
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
|
||||||
return $episodeAirDate >= $monitorStartDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
|
||||||
{
|
|
||||||
return $downloadedEpisodes->filter(
|
|
||||||
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
|
||||||
&& $episode->season === $episodeInShow->seasonNumber
|
|
||||||
)->count() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
|
|
||||||
{
|
|
||||||
return $this->monitorRepository->findOneBy([
|
|
||||||
'imdbId' => $monitor->getImdbId(),
|
|
||||||
'title' => $monitor->getTitle(),
|
|
||||||
'monitorType' => 'tvepisode',
|
|
||||||
'season' => $episode->seasonNumber,
|
|
||||||
'episode' => $episode->episodeNumber,
|
|
||||||
'status' => ['New', 'Active', 'In Progress']
|
|
||||||
]) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function refreshData(Monitor $monitor)
|
|
||||||
{
|
|
||||||
if (null === $monitor->getPoster()) {
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
|
||||||
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster;
|
|
||||||
if (null !== $poster && "" !== $poster) {
|
|
||||||
$monitor->setPoster($poster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,13 +29,10 @@ class CalendarController extends AbstractController
|
|||||||
|
|
||||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
$calendar->event(Map::from($monitors)->map(function (Monitor $monitor) {
|
$calendar->event(Map::from($monitors)->map(function (Monitor $monitor) {
|
||||||
$event = new Event($monitor->getTitle())
|
return new Event($monitor->getTitle())
|
||||||
->startsAt($monitor->getAirDate())
|
->startsAt($monitor->getAirDate())
|
||||||
|
->attachment($monitor->getPoster())
|
||||||
->fullDay();
|
->fullDay();
|
||||||
if (null !== $monitor->getPoster()) {
|
|
||||||
$event->attachment($monitor->getPoster());
|
|
||||||
}
|
|
||||||
return $event;
|
|
||||||
})->toArray());
|
})->toArray());
|
||||||
|
|
||||||
return new Response($calendar->get(), 200, [
|
return new Response($calendar->get(), 200, [
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class WebController extends AbstractController
|
|||||||
private readonly Environment $renderer,
|
private readonly Environment $renderer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/monitors', name: 'app.monitors', methods: ['GET'])]
|
#[Route('/monitors', name: 'app_monitors', methods: ['GET'])]
|
||||||
public function addMonitor()
|
public function addMonitor()
|
||||||
{
|
{
|
||||||
return $this->render('monitor/index.html.twig');
|
return $this->render('monitor/index.html.twig');
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class MonitorDispatcher
|
|||||||
'tvshows' => MonitorTvShowCommand::class,
|
'tvshows' => MonitorTvShowCommand::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
$monitors = $this->monitorRepository->findBy([
|
||||||
|
'status' => ['New', 'Active'],
|
||||||
|
]);
|
||||||
|
|
||||||
foreach ($monitors as $monitor) {
|
foreach ($monitors as $monitor) {
|
||||||
$monitor->setStatus('In Progress');
|
$monitor->setStatus('In Progress');
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?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,18 +2,13 @@
|
|||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -52,50 +47,4 @@ 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,7 +7,6 @@ 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;
|
||||||
@@ -29,7 +28,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,17 +87,6 @@ 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,11 +6,9 @@ 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;
|
||||||
@@ -21,7 +19,6 @@ use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
|
|||||||
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
||||||
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
||||||
use Tmdb\Event\RequestEvent;
|
use Tmdb\Event\RequestEvent;
|
||||||
use Tmdb\Repository\DiscoverRepository;
|
|
||||||
use Tmdb\Repository\MovieRepository;
|
use Tmdb\Repository\MovieRepository;
|
||||||
use Tmdb\Repository\SearchRepository;
|
use Tmdb\Repository\SearchRepository;
|
||||||
use Tmdb\Repository\TvEpisodeRepository;
|
use Tmdb\Repository\TvEpisodeRepository;
|
||||||
@@ -33,7 +30,6 @@ 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';
|
|
||||||
|
|
||||||
protected Client $client;
|
protected Client $client;
|
||||||
protected MovieRepository $movieRepository;
|
protected MovieRepository $movieRepository;
|
||||||
@@ -42,8 +38,6 @@ class TmdbClient
|
|||||||
protected TvEpisodeRepository $tvEpisodeRepository;
|
protected TvEpisodeRepository $tvEpisodeRepository;
|
||||||
protected SearchRepository $searchRepository;
|
protected SearchRepository $searchRepository;
|
||||||
|
|
||||||
protected DiscoverRepository $discoverRepository;
|
|
||||||
|
|
||||||
protected array $mediaTypeMap = [
|
protected array $mediaTypeMap = [
|
||||||
MediaType::Movie->value => MediaType::Movie->value,
|
MediaType::Movie->value => MediaType::Movie->value,
|
||||||
MediaType::TvShow->value => MediaType::TvShow->value,
|
MediaType::TvShow->value => MediaType::TvShow->value,
|
||||||
@@ -54,14 +48,11 @@ class TmdbClient
|
|||||||
|
|
||||||
protected $repos = [];
|
protected $repos = [];
|
||||||
|
|
||||||
private ?string $originalLanguage = 'en';
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
#[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null,
|
|
||||||
) {
|
) {
|
||||||
$this->client = new Client(
|
$this->client = new Client(
|
||||||
[
|
[
|
||||||
@@ -83,7 +74,7 @@ class TmdbClient
|
|||||||
'hydration' => [
|
'hydration' => [
|
||||||
'event_listener_handles_hydration' => false,
|
'event_listener_handles_hydration' => false,
|
||||||
'only_for_specified_models' => []
|
'only_for_specified_models' => []
|
||||||
],
|
]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,14 +107,11 @@ class TmdbClient
|
|||||||
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
|
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
|
||||||
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
|
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
|
||||||
$this->searchRepository = new SearchRepository($this->client);
|
$this->searchRepository = new SearchRepository($this->client);
|
||||||
$this->discoverRepository = new DiscoverRepository($this->client);
|
|
||||||
|
|
||||||
$this->repos = [
|
$this->repos = [
|
||||||
MediaType::Movie->value => $this->movieRepository,
|
MediaType::Movie->value => $this->movieRepository,
|
||||||
MediaType::TvShow->value => $this->tvRepository,
|
MediaType::TvShow->value => $this->tvRepository,
|
||||||
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
|
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
|
||||||
];
|
];
|
||||||
$this->originalLanguage = $originalLanguage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function search(string $term): TmdbResult|Map
|
public function search(string $term): TmdbResult|Map
|
||||||
@@ -141,55 +129,11 @@ 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'];
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]),
|
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids,credits']),
|
||||||
MediaType::Movie->value,
|
MediaType::Movie->value,
|
||||||
$imdbId
|
$imdbId
|
||||||
);
|
);
|
||||||
@@ -198,7 +142,7 @@ class TmdbClient
|
|||||||
public function tvshowDetails(string $imdbId): ?TmdbResult
|
public function tvshowDetails(string $imdbId): ?TmdbResult
|
||||||
{
|
{
|
||||||
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]);
|
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids,credits']);
|
||||||
|
|
||||||
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
|
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
|
||||||
return $data['season_number'] !== 0 &&
|
return $data['season_number'] !== 0 &&
|
||||||
@@ -219,7 +163,7 @@ class TmdbClient
|
|||||||
|
|
||||||
public function tvSeasonDetails(string $tmdbId, int $season): array
|
public function tvSeasonDetails(string $tmdbId, int $season): array
|
||||||
{
|
{
|
||||||
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => static::APPEND_TO_RESPONSE]);
|
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => 'external_ids,credits']);
|
||||||
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
|
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
|
||||||
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
|
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
|
||||||
$data['poster'] = $data['still_path'];
|
$data['poster'] = $data['still_path'];
|
||||||
@@ -230,7 +174,7 @@ class TmdbClient
|
|||||||
|
|
||||||
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
||||||
{
|
{
|
||||||
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => static::APPEND_TO_RESPONSE]);
|
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$result,
|
$result,
|
||||||
MediaType::TvEpisode->value,
|
MediaType::TvEpisode->value,
|
||||||
@@ -247,20 +191,9 @@ 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->movieRepository->getApi()->getPopular();
|
||||||
'with_original_language' => $this->originalLanguage,
|
|
||||||
'append_to_response' => 'external_ids,watch/providers',
|
|
||||||
]);
|
|
||||||
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
$result['media_type'] = MediaType::Movie->value;
|
$result['media_type'] = MediaType::Movie->value;
|
||||||
return $result;
|
return $result;
|
||||||
@@ -273,10 +206,7 @@ class TmdbClient
|
|||||||
|
|
||||||
public function popularTvShows(int $resultCount = 6): Map
|
public function popularTvShows(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
$results = $this->discoverRepository->getApi()->discoverTv([
|
$results = $this->tvRepository->getApi()->getPopular();
|
||||||
'with_original_language' => $this->originalLanguage,
|
|
||||||
'append_to_response' => 'external_ids,watch/providers',
|
|
||||||
]);
|
|
||||||
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
$result['media_type'] = MediaType::TvShow->value;
|
$result['media_type'] = MediaType::TvShow->value;
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?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,7 +7,6 @@ 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;
|
||||||
|
|
||||||
@@ -58,6 +57,5 @@ 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
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
@@ -68,10 +68,4 @@ class Torrentio
|
|||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDestinationUrl(string $url)
|
|
||||||
{
|
|
||||||
$request = get_headers($url)[8];
|
|
||||||
return explode(' ', $request)[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
|||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||||
use App\Torrentio\Client\Torrentio;
|
|
||||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
@@ -22,8 +21,6 @@ use Symfony\UX\Turbo\TurboBundle;
|
|||||||
|
|
||||||
final class WebController extends AbstractController
|
final class WebController extends AbstractController
|
||||||
{
|
{
|
||||||
const REAL_DEBRID_STREAM_URL = 'https://real-debrid.com/streaming-%s';
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
@@ -102,14 +99,4 @@ final class WebController extends AbstractController
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/torrentio/stream/{url}', name: 'app.torrentio.stream')]
|
|
||||||
public function streamVideo(string $url, Torrentio $torrentio): Response
|
|
||||||
{
|
|
||||||
$destinationUrl = $torrentio->getDestinationUrl(\base64_decode($url));
|
|
||||||
$urlPathParts = explode('/', parse_url($destinationUrl)['path']);
|
|
||||||
$videoId = $urlPathParts[2];
|
|
||||||
$url = sprintf(self::REAL_DEBRID_STREAM_URL, $videoId);
|
|
||||||
return $this->redirect($url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
@@ -112,18 +112,4 @@ class UtilExtension
|
|||||||
|
|
||||||
return new EpisodeIdDto($season, $episode);
|
return new EpisodeIdDto($season, $episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[AsTwigFunction('sentry_enabled')]
|
|
||||||
public function sentryEnabled(): bool
|
|
||||||
{
|
|
||||||
$sentryConfig = $this->config->getSentryConfig();
|
|
||||||
return $sentryConfig['javascript_url'] !== null &&
|
|
||||||
$sentryConfig['javascript_url'] !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
#[AsTwigFilter('base64_encode')]
|
|
||||||
public function base64_encode(string $data): string
|
|
||||||
{
|
|
||||||
return \base64_encode($data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||||
<script src="https://bugs.caldwell.digital/js-sdk-loader/8dddf7fb26fbec602ad212173a942450.min.js" crossorigin="anonymous"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cyan-950 flex flex-col h-full">
|
<body class="bg-cyan-950 flex flex-col h-full">
|
||||||
|
|||||||
@@ -4,18 +4,21 @@
|
|||||||
<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 %}Torsearch{% endblock %}</title>
|
<title>{% block title %}Welcome!{% 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') }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block pre_js %}{% endblock %}
|
||||||
|
|
||||||
|
{% block importmap %}
|
||||||
|
{{ importmap('app') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block post_js %}{% endblock %}
|
||||||
|
|
||||||
{% if sentry_enabled() %}
|
|
||||||
<script src="{{ sentry_javascript_url }}" crossorigin="anonymous"></script>
|
|
||||||
{% endif %}
|
|
||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@@ -26,9 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
||||||
<twig:Header />
|
<twig:Header />
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="px-4 mt-4 mb-4 md:mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
<h2 class="px-4 mt-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||||
<div class="flex mx-4 mb-2 md:mt-4 gap-2 items-center grow-0 md:px-4">
|
<div class="flex mt-4 gap-2 items-center grow-0 md:px-4">
|
||||||
{% block action_buttons %}{% endblock %}
|
{% block action_buttons %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||||
<tr id="monitor_view_all">
|
<tr id="monitor_view_all">
|
||||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
<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>
|
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app.monitors') }}"
|
<a href="{{ path('app_monitors') }}"
|
||||||
class="block rounded-lg
|
class="block rounded-lg
|
||||||
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
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">
|
px-4 py-2 text-sm font-medium text-gray-50">
|
||||||
@@ -29,15 +29,6 @@
|
|||||||
</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,21 +1,14 @@
|
|||||||
<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
|
||||||
}) }}">
|
}) }}">
|
||||||
<h3 class="mt-2 text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
<img src="{{ preload(image) }}" class="w-full md:w-40 rounded-md" />
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<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="{{ preload(poster) }}" />
|
<img class="w-full md:w-24 rounded-lg" src="{{ 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" />
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -4,20 +4,31 @@
|
|||||||
{% block h2 %}Dashboard{% endblock %}
|
{% block h2 %}Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col grow gap-4 z-10">
|
<div class="p-4 z-10">
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="grid-stack gs-2">
|
||||||
<twig:Card title="Active Downloads" class="w-full">
|
<div class="grid-stack-item" gs-x="1">
|
||||||
<twig:DownloadList :type="'active'" />
|
<div class="grid-stack-item-content">
|
||||||
</twig:Card>
|
<twig:Card title="Active Downloads">
|
||||||
|
<twig:DownloadList :type="'active'" />
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<twig:Card title="Recent Downloads" class="w-full">
|
<div class="grid-stack-item" gs-x="2">
|
||||||
<twig:DownloadList :type="'complete'" />
|
<div class="grid-stack-item-content">
|
||||||
</twig:Card>
|
<twig:Card title="Complete Downloads" >
|
||||||
</div>
|
<twig:DownloadList :type="'complete'" />
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
</twig:Card>
|
||||||
<twig:Card title="Monitors" class="w-full">
|
</div>
|
||||||
<twig:MonitorList :type="'active'" :isWidget="true" />
|
</div>
|
||||||
</twig:Card>
|
|
||||||
|
<div class="grid-stack-item" gs-x="3">
|
||||||
|
<div class="grid-stack-item-content">
|
||||||
|
<twig:Card title="Active Monitors">
|
||||||
|
<twig:MonitorList :type="'active'" />
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<twig:Card title="Popular Movies" contentClass="grid grid-cols-2 gap-4 md:flex md:flex-row md:justify-between w-full">
|
<twig:Card title="Popular Movies" contentClass="grid grid-cols-2 gap-4 md:flex md:flex-row md:justify-between w-full">
|
||||||
@@ -45,5 +56,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-stack" data-controller="dashboard-widgets"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@
|
|||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="mb-4 text-white">
|
|
||||||
<a href="{{ path('app.monitors') }}" class="btn btn-primary inline-flex items-center gap-2">
|
|
||||||
<twig:ux:icon name="fluent-mdl2:back" width="14" /> Back to Monitors
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<twig:Card title="Upcoming episodes of shows your monitoring">
|
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||||
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
||||||
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||||
|
|||||||
@@ -5,11 +5,6 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="px-4 py-2">
|
<div class="px-4 py-2">
|
||||||
<div class="mb-4 text-white">
|
|
||||||
<a href="{{ path('app.monitors') }}" class="btn btn-primary inline-flex items-center gap-2">
|
|
||||||
<twig:ux:icon name="fluent-mdl2:back" width="14" /> Back to Monitors
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<twig:Card title="Viewing your monitors for {{ monitor.title }}">
|
<twig:Card title="Viewing your monitors for {{ monitor.title }}">
|
||||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
{% if results.media.poster != null %}
|
{% if results.media.poster != null %}
|
||||||
@@ -58,6 +53,7 @@
|
|||||||
|
|
||||||
{% 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">
|
||||||
|
{# <strong>Genres</strong>: <br />#}
|
||||||
{% 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>
|
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -69,10 +65,10 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col grow text-white">
|
<div class="flex flex-col grow text-white">
|
||||||
<strong class="mb-1">In Your Library</strong>
|
<strong class="mb-1">In Your Library</strong>
|
||||||
<div class="flex flex-col md:flex-row border-t-orange-500 text-xs gap-4">
|
<div class="flex flex-col md:flex-row border-t-orange-500 text-xs gap-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm mb-1">Seasons</span>
|
<span class="text-sm mb-1">Seasons</span>
|
||||||
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
<span>{{ library.seasonCount }}</span> full
|
<span>{{ library.seasonCount }}</span> full
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +83,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm mb-1">Episodes</span>
|
<span class="text-sm mb-1">Episodes</span>
|
||||||
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
||||||
<span>{{ library.episodeCount }}</span> existing
|
<span>{{ library.episodeCount }}</span> existing
|
||||||
</span>
|
</span>
|
||||||
@@ -99,14 +95,14 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm mb-1">Monitors</span>
|
<span class="text-sm mb-1">Monitors</span>
|
||||||
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
<div class="flex flex-col md:flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
<span>{{ library.monitorCount }}</span> total
|
<span>{{ library.monitorCount }}</span> total
|
||||||
</span>
|
</span>
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
<span>{{ library.activeMonitorCount }}</span> active
|
<span>{{ library.activeMonitorCount }}</span> active
|
||||||
</span>
|
</span>
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-pink-500 rounded-lg text-white">
|
||||||
<span>{{ library.completeMonitorCount }}</span> complete
|
<span>{{ library.completeMonitorCount }}</span> complete
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,57 +76,48 @@
|
|||||||
{% 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 %}
|
||||||
<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>
|
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
<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="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
|
<span>{{ results.media.numberSeasons }}</span> season(s)
|
||||||
<span>{{ results.media.numberSeasons }}</span> season(s)
|
</span>
|
||||||
</span>
|
<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 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') }}.'>
|
{{ results.media.premiereDate|date(null, 'UTC') }}
|
||||||
{{ results.media.premiereDate|date(null, 'UTC') }}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if "movies" == results.media.mediaType %}
|
|
||||||
<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-number" id="movie_results_count">-</span> results
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
|
||||||
title: results.media.title,
|
|
||||||
block: 'media_exists_badge',
|
|
||||||
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'
|
|
||||||
}) }}">
|
|
||||||
</twig:Turbo:Frame>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "movies" == results.media.mediaType %}
|
||||||
|
<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-number" id="movie_results_count">-</span> results
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||||
|
title: results.media.title,
|
||||||
|
block: 'media_exists_badge',
|
||||||
|
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 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -86,18 +86,9 @@
|
|||||||
{{ result.languageFlags|raw }}
|
{{ result.languageFlags|raw }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
||||||
<a href="{{ url('app.torrentio.stream', {url: result.url|base64_encode}) }}" title="Stream in your browser." class="p-1.5 bg-blue-600 rounded-md text-gray-50">
|
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50">
|
||||||
<twig:ux:icon name="bi:camera-video" width="20"/>
|
Download
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50" title="Download to your mounted directory.">
|
|
||||||
<twig:ux:icon name="bi:cloud-download" width="20" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="{{ result.url }}" title="Download to your local device." class="p-1.5 bg-orange-500 rounded-md text-gray-50">
|
|
||||||
<twig:ux:icon name="bi:download" width="20" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<label for="select">
|
<label for="select">
|
||||||
<input id="select" type="checkbox" name="select" />
|
<input id="select" type="checkbox" name="select" />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{% extends 'base.html.twig' %}
|
|
||||||
|
|
||||||
{% block title %}Streaming — Torrentio{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<iframe width="100%" height="100%" src="{{ stream_url }}"></iframe>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user