Compare commits

..

8 Commits

47 changed files with 534 additions and 1037 deletions

8
.env
View File

@@ -51,11 +51,3 @@ OIDC_CLIENT_ID="Enter your OIDC client id"
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
OIDC_BYPASS_FORM_LOGIN=false
###< drenso/symfony-oidc-bundle ###
SMTP_HOST=
SMTP_USER=
SMTP_PASS=
SMTP_PORT=
SMTP_FROM=
SMTP_FROM_NAME=""

View File

@@ -20,24 +20,19 @@ export default class extends Controller {
options = []
optionsLoaded = false
resultCountEl = null
async connect() {
await this.setOptions();
this.resultCountEl = document.querySelector('#movie_results_count');
}
async setOptions() {
if (false === this.optionsLoaded) {
this.optionsLoaded = true;
await fetch(`/torrentio/movies/${this.tmdbIdValue}/${this.imdbIdValue}`)
.then(res => res.text())
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.dispatch('optionsLoaded', {detail: {options: this.options}})
this.loadingIconOutlet.toggleIcon();
});
}
async listTargetConnected() {
this.optionsLoaded = true;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.dispatch('optionsLoaded', {detail: {options: this.options}})
this.loadingIconOutlet.toggleIcon();
this.resultCountEl.innerText = this.options.length;
}
// Keeps compatible with Filter & TV Shows
@@ -102,5 +97,6 @@ export default class extends Controller {
count = count + 1;
}
});
this.resultCountEl.innerText = count;
}
}

View File

@@ -25,37 +25,18 @@ export default class extends Controller {
optionsLoaded = false
isOpen = false
async connect() {
await this.setOptions();
}
async setOptions() {
if (this.optionsLoaded === false) {
this.optionsLoaded = true;
let response;
try {
response = await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
} catch (error) {
console.log('There was an error', error);
}
if (response?.ok) {
response = await response.text()
this.listContainerTarget.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
if (this.options.length > 0) {
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.options[0].querySelector('input[type="checkbox"]').checked = true;
} else {
this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true;
}
this.dispatch('optionsLoaded', {detail: {options: this.options}})
this.loadingIconOutlet.increaseCount();
} else {
console.log(`HTTP Response Code: ${response?.status}`)
}
async listTargetConnected() {
this.options = this.element.querySelectorAll('tbody tr');
if (this.options.length > 0) {
this.options.forEach((option) =>
option.querySelector('.download-btn').dataset['title'] = this.titleValue
);
this.options[0].querySelector('input[type="checkbox"]').checked = true;
this.dispatch('optionsLoaded', {detail: {options: this.options}})
this.loadingIconOutlet.increaseCount();
} else {
this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true;
}
}
@@ -134,6 +115,7 @@ export default class extends Controller {
"codec": option.querySelector('#codec').textContent.trim(),
"provider": option.querySelector('#provider').textContent.trim(),
"languages": JSON.parse(option.dataset['languages']),
"quality": option.dataset['quality'],
}
let include = true;

View File

@@ -43,7 +43,6 @@
"symfony/mailer": "7.3.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "7.3.*",
"symfony/monolog-bundle": "^3.10",
"symfony/runtime": "7.3.*",
"symfony/scheduler": "7.3.*",
"symfony/security-bundle": "7.3.*",

264
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cf9a5b3cdb6e21e270da6c4efee09005",
"content-hash": "0f98dada0a01d471cebf4eb1b51b9006",
"packages": [
{
"name": "1tomany/rich-bundle",
@@ -2681,109 +2681,6 @@
},
"time": "2025-02-06T08:48:15+00:00"
},
{
"name": "monolog/monolog",
"version": "3.9.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/log": "^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"php-console/php-console": "^3.1.8",
"phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
"predis/predis": "^1.1 || ^2",
"rollbar/rollbar": "^4.0",
"ruflin/elastica": "^7 || ^8",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/3.9.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2025-03-24T10:02:05+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.10.0",
@@ -7681,165 +7578,6 @@
],
"time": "2025-02-19T08:51:26+00:00"
},
{
"name": "symfony/monolog-bridge",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
"reference": "1b188c8abbbef25b111da878797514b7a8d33990"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1b188c8abbbef25b111da878797514b7a8d33990",
"reference": "1b188c8abbbef25b111da878797514b7a8d33990",
"shasum": ""
},
"require": {
"monolog/monolog": "^3",
"php": ">=8.2",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/console": "<6.4",
"symfony/http-foundation": "<6.4",
"symfony/security-core": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/mailer": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"type": "symfony-bridge",
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Monolog\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/monolog-bridge/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-03-21T12:17:46+00:00"
},
{
"name": "symfony/monolog-bundle",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bundle.git",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"shasum": ""
},
"require": {
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
"php": ">=7.2.5",
"symfony/config": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.3 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MonologBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MonologBundle",
"homepage": "https://symfony.com",
"keywords": [
"log",
"logging"
],
"support": {
"issues": "https://github.com/symfony/monolog-bundle/issues",
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-11-06T17:08:13+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v7.3.0",

View File

@@ -23,5 +23,4 @@ return [
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
];

View File

@@ -1,78 +0,0 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
- monitor
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
monitor:
type: stream
action_level: debug
path: "%kernel.logs_dir%/monitors.log"
channels: [monitor]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
monitor:
type: stream
action_level: info
path: "%kernel.logs_dir%/monitors.log"
channels: [monitor]
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json
monitor:
type: stream
action_level: info
path: "%kernel.logs_dir%/monitors.log"
channels: [monitor]

View File

@@ -6,6 +6,14 @@ controllersBase:
defaults:
schemes: [ 'https' ]
controllersLibrary:
resource:
path: ../src/Library/Framework/Controller/
namespace: App\Library\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersSearch:
resource:
path: ../src/Search/Framework/Controller/

View File

@@ -6,7 +6,6 @@
parameters:
# App
app.url: '%env(APP_URL)%'
app.env: '%env(default:app.default.env:APP_ENV)%'
app.version: '%env(default:app.default.version:APP_VERSION)%'
# Debrid Services
@@ -33,7 +32,6 @@ parameters:
app.cache.redis.host.default: 'redis://redis'
# Various configs
app.default.env: 'prod'
app.default.version: '0.dev'
app.default.timezone: 'America/Chicago'

View File

@@ -1,105 +0,0 @@
<?php
namespace App\Base\Config;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class AppConfig implements ConfigInterface
{
private array $messages = [];
public function __construct(
#[Autowire(param: 'app.env')]
private readonly ?string $appEnv = null,
#[Autowire(param: 'app.url')]
private readonly ?string $appUrl = null,
#[Autowire(param: 'app.debrid.real_debrid.key')]
private readonly ?string $realDebridApiKey = null,
#[Autowire(param: 'app.meta_provider.tmdb.key')]
private readonly ?string $tmdbApiKey = null,
#[Autowire(param: 'media.movies_path')]
private readonly ?string $moviesPath = null,
#[Autowire(param: 'media.tvshows_path')]
private readonly ?string $tvshowsPath = null,
) {}
public function isValid(): bool
{
$valid = true;
if (false === $this->isVariableValid('APP_ENV', $this->appEnv)) {
$valid = false;
}
if (false === $this->isVariableValid('APP_URL', $this->appUrl)) {
$valid = false;
}
if (false === $this->isVariableValid('REAL_DEBRID_KEY', $this->realDebridApiKey)) {
$valid = false;
}
if (false === $this->isVariableValid('TMDB_API', $this->tmdbApiKey)) {
$valid = false;
}
if (false === $this->isVariableValid('MOVIES_PATH', $this->moviesPath)) {
$valid = false;
}
if (false === $this->isVariableValid('TVSHOWS_PATH', $this->tvshowsPath)) {
$valid = false;
}
return $valid;
}
public function getMessages(): array
{
return $this->messages;
}
private function isVariableValid($key, $value): bool
{
if ("" === $value || null === $value) {
$this->messages[] = "Your system is misconfigured. Please set the $key environment variable appropriately.";
return false;
}
return true;
}
public function getAppEnv(): ?string
{
return $this->appEnv;
}
public function getAppUrl(): ?string
{
return $this->appUrl;
}
public function getRealDebridApiKey(): ?string
{
return $this->realDebridApiKey;
}
public function getTmdbApiKey(): ?string
{
return $this->tmdbApiKey;
}
public function getMoviesPath(): ?string
{
return $this->moviesPath;
}
public function getTvshowsPath(): ?string
{
return $this->tvshowsPath;
}
}

View File

@@ -1,88 +0,0 @@
<?php
namespace App\Base\Config\Auth;
use App\Base\Config\ConfigInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class OidcConfig implements ConfigInterface
{
private array $messages = [];
public function __construct(
#[Autowire(param: 'auth.method')]
private readonly ?string $authMethod = null,
#[Autowire(param: 'auth.oidc.well_known_url')]
private readonly ?string $wellKnownUrl = null,
#[Autowire(param: 'auth.oidc.client_id')]
private readonly ?string $clientId = null,
#[Autowire(param: 'auth.oidc.client_secret')]
private readonly ?string $clientSecret = null,
#[Autowire(param: 'auth.oidc.bypass_form_login')]
private readonly ?bool $bypassFormLogin = null,
) {}
public function isEnabled(): bool
{
return "oidc" === strtolower($this->authMethod);
}
public function isValid(): bool
{
$valid = true;
if (true === $this->isEnabled()) {
if (false === $this->isVariableValid("OIDC_CLIENT_ID", $this->clientId)) {
$valid = false;
}
if (false === $this->isVariableValid("OIDC_CLIENT_SECRET", $this->clientSecret)) {
$valid = false;
}
if (false === $this->isVariableValid("OIDC_WELL_KNOWN_URL", $this->wellKnownUrl)) {
$valid = false;
}
}
return $valid;
}
public function getWellKnownUrl(): ?string
{
return $this->wellKnownUrl;
}
public function getClientId(): ?string
{
return $this->clientId;
}
public function getClientSecret(): ?string
{
return $this->clientSecret;
}
public function getBypassFormLogin(): ?bool
{
return $this->bypassFormLogin;
}
public function getMessages(): array
{
return $this->messages;
}
private function isVariableValid(string $key, mixed $value): bool
{
if ("" === $value || null === $value) {
$this->messages[] = "Your OIDC is misconfigured. Please set the $key environment variable to the required value.";
return true;
}
return false;
}
}

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Base\Config;
use App\Base\Config\Auth\OidcConfig;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class AuthConfig implements ConfigInterface
{
const AUTH_METHODS = ['form_login', 'oidc'];
private array $messages = [];
public function __construct(
#[Autowire(param: 'auth.method')]
private readonly ?string $authMethod,
private readonly OidcConfig $oidcConfig,
) {}
public function isMethod(string $method): bool
{
return $this->getAuthMethod() === strtolower($method);
}
public function getAuthMethod(): string
{
return strtolower($this->authMethod);
}
public function getOidcConfig(): OidcConfig
{
return $this->oidcConfig;
}
public function isValid(): bool
{
$valid = true;
if (null === $this->getAuthMethod() || "" === $this->getAuthMethod()) {
$this->messages[] = "Your auth method is missing. Please set the AUTH_METHOD environment variable to your desired value. Valid options: [form_login, oidc].";
return false;
}
if (!in_array($this->getAuthMethod(), self::AUTH_METHODS, true)) {
$this->messages[] = "Your auth method is incorrect. Please set the AUTH_METHOD environment variable to your desired value. Valid options: [form_login, oidc].";
return false;
}
if ("oidc" === $this->getAuthMethod()) {
if (false === $this->oidcConfig->isValid()) {
$this->messages += $this->oidcConfig->getMessages();
$valid = false;
}
}
return $valid;
}
public function getMessages(): array
{
return $this->messages;
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Base\Config;
interface ConfigInterface
{
/** Validates the config values are present and as expected */
public function isValid(): bool;
/** Holds the error messages so they can be logged */
public function getMessages(): array;
}

View File

@@ -1,113 +0,0 @@
<?php
namespace App\Base\Config;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class SmtpConfig implements ConfigInterface
{
private array $messages = [];
/*
* SMTP is considered enabled if any of
* the parameters are set. If none are set,
* then the User must not need it.
*/
private bool $isEnabled = false;
public function __construct(
#[Autowire(env: 'SMTP_HOST')]
private readonly ?string $smtpHost,
#[Autowire(env: 'SMTP_USER')]
private readonly ?string $smtpUser,
#[Autowire(env: 'SMTP_PASS')]
private readonly ?string $smtpPass,
#[Autowire(env: 'SMTP_PORT')]
private readonly ?string $smtpPort,
#[Autowire(env: 'SMTP_FROM')]
private readonly ?string $smtpFrom,
#[Autowire(env: 'SMTP_FROM_NAME')]
private readonly ?string $smtpFromName,
) {
foreach (func_get_args() as $key => $value) {
if ("" !== $value && $value !== null) {
$this->isEnabled = true;
}
}
}
public function isEnabled(): bool
{
return $this->isEnabled;
}
public function getMessages(): array
{
return $this->messages;
}
public function isValid(): bool
{
if (false === $this->isEnabled) {
return true;
}
$valid = true;
$params = [
'SMTP_HOST' => $this->smtpHost,
'SMTP_USER' => $this->smtpUser,
'SMTP_PASS' => $this->smtpPass,
'SMTP_PORT' => $this->smtpPort,
'SMTP_FROM' => $this->smtpFrom,
'SMTP_FROM_NAME' => $this->smtpFromName,
];
foreach ($params as $key => $value) {
if (false === $this->isVariableValid($key, $value)) {
$valid = false;
}
}
return $valid;
}
private function isVariableValid($key, $value): bool
{
if ("" === $value || null === $value) {
$this->messages[] = "Your SMTP is misconfigured. Please set the $key environment variable appropriately.";
return false;
}
return true;
}
public function getSmtpHost(): ?string
{
return $this->smtpHost;
}
public function getSmtpUser(): ?string
{
return $this->smtpUser;
}
public function getSmtpPass(): ?string
{
return $this->smtpPass;
}
public function getSmtpPort(): ?string
{
return $this->smtpPort;
}
public function getSmtpFrom(): ?string
{
return $this->smtpFrom;
}
public function getSmtpFromName(): ?string
{
return $this->smtpFromName;
}
}

View File

@@ -2,60 +2,58 @@
namespace App\Base;
use App\Base\Config\AppConfig;
use App\Base\Config\AuthConfig;
use App\Base\Config\SmtpConfig;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class ConfigResolver
{
private array $messages = [];
public function __construct(
private readonly LoggerInterface $logger,
private readonly TagAwareCacheInterface $cache,
private readonly AuthConfig $authConfig,
private readonly SmtpConfig $smtpConfig,
private readonly AppConfig $appConfig,
#[Autowire(param: 'app.url')]
private readonly ?string $appUrl = null,
#[Autowire(param: 'app.debrid.real_debrid.key')]
private readonly ?string $realDebridApiKey = null,
#[Autowire(param: 'app.meta_provider.tmdb.key')]
private readonly ?string $tmdbApiKey = null,
#[Autowire(param: 'media.movies_path')]
private readonly ?string $moviesPath = null,
#[Autowire(param: 'media.tvshows.path')]
private readonly ?string $tvshowsPath = null,
#[Autowire(param: 'auth.method')]
private readonly ?string $authMethod = null,
#[Autowire(param: 'auth.oidc.well_known_url')]
private readonly ?string $authOidcWellKnownUrl = null,
#[Autowire(param: 'auth.oidc.client_id')]
private readonly ?string $authOidcClientId = null,
#[Autowire(param: 'auth.oidc.client_secret')]
private readonly ?string $authOidcClientSecret = null,
#[Autowire(param: 'auth.oidc.bypass_form_login')]
private ?bool $authOidcBypassFormLogin = null,
) {}
public function validate(): bool
{
if ("prod" === strtolower($this->appConfig->getAppEnv())) {
return $this->cache->get('app.valid_config', function () {
return $this->doValidate();
});
} else {
return $this->doValidate();
}
}
private function doValidate(): bool
{
$valid = true;
if (false === $this->appConfig->isValid()) {
$this->messages += $this->appConfig->getMessages();
if (null === $this->realDebridApiKey || "" === $this->realDebridApiKey) {
$this->messages[] = "Your Real Debrid API key is missing. Please set it to the 'REAL_DEBRID_KEY' environment variable.";
$valid = false;
}
if (false === $this->authConfig->isValid()) {
$this->messages += $this->authConfig->getMessages();
if (null === $this->tmdbApiKey || "" === $this->tmdbApiKey) {
$this->messages[] = "Your TMDB API key is missing. Please set it to the 'TMDB_API' environment variable.";
$valid = false;
}
if (false === $this->smtpConfig->isValid()) {
$this->messages += $this->smtpConfig->getMessages();
$valid = false;
}
if (false === $valid) {
foreach ($this->messages as $message) {
$this->logger->error('> [ConfigResolver] ' . $message);
}
}
return $valid;
}
@@ -64,18 +62,34 @@ final class ConfigResolver
return $this->messages;
}
public function getAuthConfig(): AuthConfig
{
return $this->authConfig;
}
public function authIs(string $method): bool
{
return $this->authConfig->isMethod($method);
if (strtolower($method) === strtolower($this->getAuthMethod())) {
return true;
}
return false;
}
public function getAuthMethod(): string
{
return $this->authConfig->getAuthMethod();
return strtolower($this->authMethod);
}
public function bypassFormLogin(): bool
{
return $this->authOidcBypassFormLogin;
}
public function getAuthConfig(): array
{
return [
'method' => $this->authMethod,
'oidc' => [
'well_known_url' => $this->authOidcWellKnownUrl,
'client_id' => $this->authOidcClientId,
'client_secret' => $this->authOidcClientSecret,
'bypass_form_login' => $this->authOidcBypassFormLogin,
]
];
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Base\Framework\EventListener;
use App\Base\ConfigResolver;
use App\Base\Service\Broadcaster;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
final class KernelRequestValidateConfig
{
public function __construct(
private readonly ConfigResolver $configResolver,
private readonly Broadcaster $broadcaster,
) {}
#[AsEventListener(event: 'kernel.request', priority: 20)]
public function validateConfig(RequestEvent $event): void
{
if (false === $this->configResolver->validate()) {
$this->broadcaster->systemAlert('Ruh-roh', 'It looks like your system is misconfigured. Please search the application logs for strings starting with "[error] > [ConfigResolver]" to find more information.');
}
}
}

View File

@@ -15,8 +15,7 @@ readonly class Broadcaster
private Environment $renderer,
private HubInterface $hub,
private RequestStack $requestStack,
) {
}
) {}
public function alert(string $title, string $message, string $type = "success"): void
{
@@ -32,18 +31,4 @@ readonly class Broadcaster
);
$this->hub->publish($update);
}
public function systemAlert(string $title, string $message, string $type = "warning"): void
{
$update = new Update(
'system_alerts',
$this->renderer->render('broadcast/Alert.stream.html.twig', [
'alert_id' => uniqid(),
'title' => $title,
'message' => $message,
'type' => $type,
])
);
$this->hub->publish($update);
}
}

View File

@@ -140,7 +140,7 @@ class MediaFiles
return $path;
}
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber)
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber): SplFileInfo|false
{
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Library\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class LibrarySearchCommand implements CommandInterface
{
public function __construct(
public ?string $term = null,
public ?string $title = null,
public ?string $imdbId = null,
public ?string $season = null,
public ?string $episode = null,
) {}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Library\Action\Handler;
use App\Base\Service\MediaFiles;
use App\Library\Action\Command\LibrarySearchCommand;
use App\Library\Action\Result\LibrarySearchResult;
use App\Library\Dto\MediaFileDto;
use Nihilarr\PTN;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
/**
* @implements HandlerInterface<LibrarySearchCommand,LibrarySearchHandler>
*/
class LibrarySearchHandler implements HandlerInterface
{
private array $searchTypes = [
'episode_by_title' => 'episodeByTitle',
'movie_by_title' => 'movieByTitle',
];
public function __construct(
private readonly MediaFiles $mediaFiles,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$searchType = $this->getSearchType($command);
$function = $this->searchTypes[$searchType];
return $this->$function($command);
}
private function getSearchType(CommandInterface $command): ?string
{
if (!is_null($command->title) &&
is_null($command->imdbId) &&
is_null($command->season) &&
is_null($command->episode)
) {
return 'movie_by_title';
}
if ((!is_null($command->title) || is_null($command->imdbId)) &&
!is_null($command->season) &&
!is_null($command->episode)
) {
return 'episode_by_title';
}
return null;
}
private function episodeByTitle(CommandInterface $command): ?LibrarySearchResult
{
$result = $this->mediaFiles->episodeExists(
$command->title,
(int) $command->season,
(int) $command->episode,
);
$exists = $result instanceof \SplFileInfo;
return new LibrarySearchResult(
input: $command,
exists: $exists,
file: true === $exists ? MediaFileDto::fromSplFileInfo($result) : null,
ptn: true === $exists ? (object) new PTN()->parse($result->getFilename()) : null,
);
}
private function movieByTitle(CommandInterface $command): ?LibrarySearchResult
{
$result = $this->mediaFiles->movieExists($command->title);
$exists = $result instanceof \SplFileInfo;
return new LibrarySearchResult(
input: $command,
exists: $exists,
file: true === $exists ? MediaFileDto::fromSplFileInfo($result) : null,
ptn: true === $exists ? (object) new PTN()->parse($result->getFilename()) : null,
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Library\Action\Input;
use App\Library\Action\Command\LibrarySearchCommand;
use OneToMany\RichBundle\Attribute\SourceQuery;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/**
* @implements InputInterface<LibrarySearchInput, LibrarySearchCommand>
*/
class LibrarySearchInput implements InputInterface
{
public function __construct(
#[SourceQuery('term', nullify: true)]
private ?string $term = null,
#[SourceQuery('title', nullify: true)]
private ?string $title = null,
#[SourceQuery('imdbId', nullify: true)]
private ?string $imdbId = null,
#[SourceQuery('season', nullify: true)]
private ?string $season = null,
#[SourceQuery('episode', nullify: true)]
private ?string $episode = null,
) {}
public function toCommand(): CommandInterface
{
return new LibrarySearchCommand(
term: $this->term,
title: $this->title,
imdbId: $this->imdbId,
season: $this->season,
episode: $this->episode,
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Library\Action\Result;
use App\Library\Dto\MediaFileDto;
use OneToMany\RichBundle\Contract\ResultInterface;
class LibrarySearchResult implements ResultInterface
{
public function __construct(
public object|array $input,
public bool $exists,
public ?MediaFileDto $file = null,
public ?object $ptn = null,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Library\Dto;
readonly class MediaFileDto
{
public function __construct(
public string $path,
public string $filename,
public string $extension,
public string $size,
) {}
public static function fromSplFileInfo(\SplFileInfo $fileInfo): self
{
return new static(
path: $fileInfo->getRealPath(),
filename: $fileInfo->getFilename(),
extension: $fileInfo->getExtension(),
size: $fileInfo->getSize(),
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Library\Framework\Controller;
use App\Library\Action\Handler\LibrarySearchHandler;
use App\Library\Action\Input\LibrarySearchInput;
use App\Library\Action\Result\LibrarySearchResult;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\UX\Turbo\TurboBundle;
class Api extends AbstractController
{
#[Route('/api/library/search', name: 'api.library.search', methods: ['GET'])]
public function search(LibrarySearchInput $input, LibrarySearchHandler $handler, Request $request): Response
{
$result = $handler->handle($input->toCommand());
if ($request->headers->get('Turbo-Frame')) {
return $this->sendFragmentResponse($result, $request);
}
return $this->json($handler->handle($input->toCommand()));
}
private function sendFragmentResponse(LibrarySearchResult $result, Request $request): Response
{
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->renderBlock(
'search/fragments.html.twig',
$request->query->get('block'),
[
'result' => $result,
'target' => $request->query->get('target')
]
);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Monitor\Action\Handler;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Monitor\Action\Command\MonitorMovieCommand;
use App\Monitor\Action\Result\MonitorMovieResult;
use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Framework\Repository\MonitorRepository;
use App\Monitor\Service\MonitorOptionEvaluator;
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Messenger\MessageBusInterface;
/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorMovieHandler implements HandlerInterface
{
public function __construct(
private MonitorRepository $movieMonitorRepository,
private GetMovieOptionsHandler $getMovieOptionsHandler,
private EntityManagerInterface $entityManager,
private MessageBusInterface $bus,
private LoggerInterface $logger,
private Security $security,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$this->logger->info('> [MonitorMovieHandler] Executing MonitorMovieHandler');
/** @var Monitor $monitor */
$monitor = $this->movieMonitorRepository->find($command->movieMonitorId);
$monitor->setStatus('In Progress');
$this->logger->info('> [MonitorMovieHandler] Searching for "' . $monitor->getTitle() . '" download options');
$results = $this->getMovieOptionsHandler->handle(
new GetMovieOptionsCommand($monitor->getTmdbId(), $monitor->getImdbId())
);
$this->logger->info('> [MonitorMovieHandler] Found ' . count($results->results) . ' download options');
$result = $this->monitorOptionEvaluator->evaluateOptions($monitor, $results->results);
if (null !== $result) {
$this->logger->info('> [MonitorMovieHandler] 1 result found: dispatching DownloadMediaCommand for "' . $result->title . '"');
$this->bus->dispatch(new DownloadMediaCommand(
$result->url,
$monitor->getTitle(),
$result->filename,
'movies',
$monitor->getImdbId(),
$monitor->getUser()->getId(),
));
$monitor->setStatus('Complete');
$monitor->setDownloadedAt(new DateTimeIMmutable());
} else {
$monitor->setStatus('Active');
}
$monitor->setLastSearch(new DateTimeImmutable());
$monitor->incrementSearchCount();
$this->entityManager->flush();
return new MonitorMovieResult(
status: 'OK',
result: [
'monitor' => $monitor,
]
);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Monitor\Action\Handler;
use App\Base\Util\EpisodeId;
use App\Download\Action\Command\DownloadMediaCommand;
use App\Download\DownloadOptionEvaluator;
use App\Download\Framework\Entity\Download;
use App\Download\Framework\Repository\DownloadRepository;
use App\Monitor\Action\Command\MonitorMovieCommand;
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
@@ -15,6 +16,7 @@ use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
use App\User\Dto\UserPreferencesFactory;
use Carbon\Carbon;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
@@ -27,6 +29,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
public function __construct(
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
private DownloadOptionEvaluator $downloadOptionEvaluator,
private EntityManagerInterface $entityManager,
private MessageBusInterface $bus,
private LoggerInterface $logger,
private MonitorRepository $monitorRepository,

View File

@@ -18,22 +18,19 @@ use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
/** @implements HandlerInterface<MonitorMovieCommand> */
readonly class MonitorTvShowHandler implements HandlerInterface
{
public function __construct(
#[Target('monitorLogger')]
private LoggerInterface $logger,
private MonitorRepository $monitorRepository,
private EntityManagerInterface $entityManager,
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
private MediaFiles $mediaFiles,
private LoggerInterface $logger,
private Tmdb $tmdb,
) {}
public function handle(CommandInterface $command): ResultInterface
{
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');

View File

@@ -9,7 +9,6 @@ use App\Monitor\Action\Command\MonitorTvShowCommand;
use App\Monitor\Framework\Repository\MonitorRepository;
use Carbon\Carbon;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
@@ -17,14 +16,13 @@ use Symfony\Component\Scheduler\Attribute\AsCronTask;
class MonitorDispatcher
{
public function __construct(
#[Target('monitorLogger')]
private readonly LoggerInterface $logger,
private readonly MonitorRepository $monitorRepository,
private readonly MessageBusInterface $bus,
) {}
public function __invoke() {
$this->logger->info('[MonitorDispatcher] > Executing MonitorDispatcher');
$this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher');
$this->cleanupStuckMonitors();
@@ -36,19 +34,15 @@ class MonitorDispatcher
];
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
$this->logger->info('[MonitorDispatcher] ' . count($monitors) . ' monitors found');
foreach ($monitors as $monitor) {
$this->logger->info('[MonitorDispatcher] - Evaluating monitor ' . $monitor->getId() . ' for "' . $monitor->getTitle() . '"');
$monitor->setStatus('In Progress');
$this->monitorRepository->getEntityManager()->flush();
$command = $monitorHandlers[$monitor->getMonitorType()];
$this->logger->info('[MonitorDispatcher] Dispatching ' . $command . ' for ' . $monitor->getTitle());
$this->logger->info('[MonitorDispatcher] Dispatching ' . $command . ' for ' . $monitor->getTitle());
$this->bus->dispatch(new $command($monitor->getId()));
}
$this->logger->info('[MonitorDispatcher] < Complete');
}
private function cleanupStuckMonitors(): void

View File

@@ -2,7 +2,6 @@
namespace App\Search\Action\Handler;
use App\Base\Service\MediaFiles;
use App\Search\Action\Command\GetMediaInfoCommand;
use App\Search\Action\Result\GetMediaInfoResult;
use App\Tmdb\Tmdb;
@@ -15,19 +14,12 @@ class GetMediaInfoHandler implements HandlerInterface
{
public function __construct(
private readonly Tmdb $tmdb,
private readonly MediaFiles $mediaFiles
) {}
public function handle(CommandInterface $command): ResultInterface
{
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
if ("tvshows" === $command->mediaType) {
foreach ($media->episodes[$command->season] as $key => $episode) {
$media->episodes[$command->season][$key]['file'] = $this->mediaFiles->episodeExists($media->title, $command->season, $episode['episode_number']);
}
}
return new GetMediaInfoResult($media, $command->season);
}
}

View File

@@ -37,7 +37,8 @@ final class WebController extends AbstractController
public function result(
GetMediaInfoInput $input,
?int $season = null,
): Response {
): Response
{
$result = $this->getMediaInfoHandler->handle($input->toCommand());
return $this->render('search/result.html.twig', [
@@ -52,32 +53,4 @@ final class WebController extends AbstractController
]
]);
}
private function warmDownloadOptionCache(TmdbResult $result)
{
if ($result->mediaType === 'tvshows') {
// dispatches a job to get the download options
// for each episode and load them in cache
foreach ($result->episodes as $season => $episodes) {
// Only do the first 2 seasons, so we reduce
// getting rate-limited by Torrentio
if ($season > 2) {
return;
}
foreach ($episodes as $episode) {
$this->bus->dispatch(new GetTvShowOptionsCommand(
tmdbId: $result->tmdbId,
imdbId: $result->imdbId,
season: $season,
episode: $episode['episode_number'],
));
}
}
} elseif ($result->mediaType === 'movies') {
$this->bus->dispatch(new GetMovieOptionsCommand(
$result->tmdbId,
$result->imdbId,
));
}
}
}

View File

@@ -301,6 +301,7 @@ class Tmdb
description: $data['overview'],
year: (new \DateTime($data['release_date']))->format('Y'),
mediaType: "movies",
episodeAirDate: (new \DateTime($data['release_date']))->format('m/d/Y'),
);
}

View File

@@ -9,12 +9,14 @@ use App\Torrentio\Action\Input\GetMovieOptionsInput;
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
use App\Torrentio\Exception\TorrentioRateLimitException;
use Carbon\Carbon;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\UX\Turbo\TurboBundle;
final class WebController extends AbstractController
{
@@ -25,7 +27,7 @@ final class WebController extends AbstractController
) {}
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache, Request $request): Response
{
$cacheId = sprintf(
"page.torrentio.movies.%s.%s",
@@ -33,9 +35,14 @@ final class WebController extends AbstractController
$input->imdbId
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
return $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
if ($request->headers->get('Turbo-Frame')) {
return $this->sendFragmentResponse($results, $request);
}
return $this->render('torrentio/movies.html.twig', [
'results' => $results,
]);
@@ -43,7 +50,7 @@ final class WebController extends AbstractController
}
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
@@ -54,13 +61,18 @@ final class WebController extends AbstractController
);
try {
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
return $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
if ($request->headers->get('Turbo-Frame')) {
return $this->sendFragmentResponse($results, $request);
}
return $this->render('torrentio/tvshows.html.twig', [
'results' => $results,
]);
// });
});
} catch (TorrentioRateLimitException $exception) {
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning');
return $this->render('bare.html.twig',
@@ -73,29 +85,16 @@ final class WebController extends AbstractController
}
}
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
private function sendFragmentResponse(ResultInterface $result, Request $request): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
$input->tmdbId,
$input->imdbId,
$input->season,
$input->episode,
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->renderBlock(
'torrentio/fragments.html.twig',
$request->query->get('block'),
[
'results' => $result,
'target' => $request->query->get('target')
]
);
$cache->delete($cacheId);
$this->broadcaster->alert(
title: 'Success',
message: 'Torrentio cache Cleared.'
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/tvshows.html.twig', [
'results' => $results,
]);
});
}
}

View File

@@ -47,7 +47,7 @@ class UtilExtension
}
#[AsTwigFilter('episode_id_from_results')]
public function episodeId($result): ?string
public function episodeIdFromResults($result): ?string
{
if (!$result instanceof GetTvShowOptionsResult) {
return null;
@@ -56,4 +56,11 @@ class UtilExtension
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
"E". str_pad($result->episode, 2, "0", STR_PAD_LEFT);
}
#[AsTwigFunction('episode_id')]
public function episodeId($season, $episode): ?string
{
return "S". str_pad($season, 2, "0", STR_PAD_LEFT) .
"E". str_pad($episode, 2, "0", STR_PAD_LEFT);
}
}

View File

@@ -22,7 +22,7 @@ class LoginController extends AbstractController
return $this->redirectToRoute('app_getting_started');
}
if ($config->authIs('oidc') && true === $config->getAuthConfig()->getOidcConfig()->getBypassFormLogin()) {
if ($config->authIs('oidc') && $config->bypassFormLogin()) {
return $this->redirectToRoute('app_login_oidc');
}

View File

@@ -3,7 +3,6 @@
namespace App\User\Framework\Controller\Web;
use App\Base\ConfigResolver;
use App\Base\Service\Broadcaster;
use Drenso\OidcBundle\OidcClientInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
@@ -16,15 +15,13 @@ class LoginOidcController extends AbstractController
public function __construct(
private ConfigResolver $configResolver,
private Broadcaster $broadcaster,
) {}
#[Route('/login/oidc', name: 'app_login_oidc')]
public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse
{
if (false === $this->configResolver->authIs('oidc')) {
$this->broadcaster->systemAlert('Your authentication must be set to "oidc" in order to login with OIDC.', 'warning');
return $this->redirectToRoute('app_login');
throw new \Exception('You must configure the OIDC environment variables before logging in at this route.');
}
// Redirect to authorization @ OIDC provider

View File

@@ -4,9 +4,13 @@ namespace App\User\Framework\Security;
use App\User\Framework\Entity\User;
use App\User\Framework\Repository\UserRepository;
use Drenso\OidcBundle\Exception\OidcException;
use Drenso\OidcBundle\Model\OidcTokens;
use Drenso\OidcBundle\Model\OidcUserData;
use Drenso\OidcBundle\Security\UserProvider\OidcUserProviderInterface;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\OidcUser;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -43,7 +47,7 @@ class OidcUserProvider implements OidcUserProviderInterface
public function supportsClass(string $class): bool
{
return User::class === $class || OidcUser::class === $class || is_subclass_of($class, User::class);
return User::class === $class || OidcUser::class === $class;
}
public function loadUserByIdentifier(string $identifier): UserInterface

View File

@@ -217,18 +217,6 @@
"config/packages/messenger.yaml"
]
},
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/property-info": {
"version": "7.3",
"recipe": {

View File

@@ -23,17 +23,5 @@
<span class="text-sm">v{{ version }}</span>
</div>
</div>
<div {{ turbo_stream_listen('system_alerts') }} class="fixed z-40 top-10 right-10">
<div class="z-40">
<ul id="alert_list" class="flex flex-col gap-2">
{% for message in app.flashes('warning') %}
<twig:Alert :title="'Warning'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %}
{% for message in app.flashes('success') %}
<twig:Alert :title="'Success'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %}
</ul>
</div>
</div>
</body>
</html>

View File

@@ -7,7 +7,7 @@
</a>
{% if download.mediaType == "tvshows" and download.episodeId != null %}
&mdash; <span class="ml-1">(S{{ download.episodeId }})</span>
&mdash; <span class="ml-1">({{ download.episodeId }})</span>
{% endif %}
</td>

View File

@@ -26,18 +26,6 @@
</div>
</div>
</div>
<div {{ turbo_stream_listen('system_alerts') }} class="fixed z-40 top-10 right-10">
<div class="z-40">
<ul id="alert_list" class="flex flex-col gap-2">
{% for message in app.flashes('system_warning') %}
<twig:Alert :title="'Warning'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %}
{% for message in app.flashes('system_success') %}
<twig:Alert :title="'Success'" :message="message" :alert_id="''" type="warning" data-controller="alert" />
{% endfor %}
</ul>
</div>
</div>
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="fixed z-40 top-10 right-10">
<div class="z-40">
<ul id="alert_list" class="flex flex-col gap-2">

View File

@@ -3,12 +3,12 @@
mediaType: mediaType,
imdbId: imdbId
}) }}">
<img src="{{ image }}" class="w-full md:w-40 rounded-md" />
<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-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
<h3 class="text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
</a>
</div>

View File

@@ -40,30 +40,17 @@
{{ episode['air_date']|date(null, 'UTC') }}
</small>
{% if episode['file'] != false %}
<span data-controller="popover">
<template data-popover-target="content">
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
<ul class="list-disc ml-3">
<li class="font-normal">{{ episode['file'].realPath|strip_media_path }} &mdash; <strong>{{ episode['file'].size|filesize }}</strong></li>
</ul>
</div>
</template>
<small
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
data-action="mouseenter->popover#show mouseleave->popover#hide"
>
exists
</small>
</span>
{% endif %}
{% if episode['file'] == false %}
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
title: this.title,
season: episode['season_number'],
episode: episode['episode_number'],
block: 'media_exists_badge',
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
}) }}">
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
missing
</small>
{% endif %}
</twig:Turbo:Frame>
</div>
</div>
<div class="flex flex-col gap-4 justify-between">
@@ -87,8 +74,15 @@
</button>
</div>
</div>
<div {{ stimulus_target('tv-results', 'listContainer') }} class="inline-block overflow-hidden rounded-lg">
<div class="inline-block overflow-hidden rounded-lg">
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
tmdbId: this.tmdbId,
imdbId: this.imdbId,
season: episode['season_number'],
episode: episode['episode_number'],
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
block: 'tvshow_results'
}) }}" />
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
{% block media_exists_badge %}
<turbo-stream action="replace" targets="#{{ target }}">
<template>
{% if result.exists == true %}
<span data-controller="popover">
<template data-popover-target="content">
<div data-popover-target="card"
class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
<p class="font-bold text-sm text-left">Existing file(s) for this media:</p>
<ul class="list-disc ml-3">
<li class="font-normal">{{ result.file.filename|strip_media_path }} &mdash; <strong>{{ result.file.size|filesize }}</strong></li>
</ul>
</div>
</template>
<small
class="py-1 px-1.5 mr-1 grow-0 font-bold bg-blue-600 rounded-lg text-center text-white"
data-action="mouseenter->popover#show mouseleave->popover#hide"
>
exists
</small>
</span>
{% endif %}
{% if result.exists == false %}
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white"
title="Media has not been downloaded yet.">
missing
</small>
{% endif %}
</template>
</turbo-stream>
{% endblock %}

View File

@@ -49,9 +49,32 @@
</div>
<p class="text-gray-50">
{{ results.media.description }}
</p>
{% if "movies" == results.media.mediaType %}
<div class="flex flex-row justify-start items-end grow">
<span class="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 id="movie_results_count">-</span> results
</span>
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Release date {{ results.media.episodeAirDate }}">
{{ results.media.episodeAirDate|date(null, 'UTC') }}
</small>
<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
}) }}">
<small 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
</small>
</twig:Turbo:Frame>
</div>
{% endif %}
</div>
</div>
@@ -62,6 +85,12 @@
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
data-movie-results-loading-icon-outlet=".loading-icon"
>
<twig:Turbo:Frame id="movie_results_frame" src="{{ path('app_torrentio_movies', {
tmdbId: results.media.tmdbId,
imdbId: results.media.imdbId,
target: 'movie_results_frame',
block: 'movie_results'
}) }}" />
</div>
{% elseif "tvshows" == results.media.mediaType %}
<twig:TvEpisodeList

View File

@@ -0,0 +1,19 @@
{% block movie_results %}
<turbo-stream action="replace" targets="#{{ target }}">
<template>
<div class="p-4 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
<div class="overflow-hidden rounded-md">
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
</div>
</div>
</template>
</turbo-stream>
{% endblock %}
{% block tvshow_results %}
<turbo-stream action="replace" targets="#{{ target }}">
<template>
{{ include('torrentio/partial/option-table.html.twig', {controller: 'tv-results'}) }}
</template>
</turbo-stream>
{% endblock %}

View File

@@ -1,20 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Hello TorrentioController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code>/var/www/src/Controller/TorrentioController.php</code></li>
<li>Your template at <code>/var/www/templates/torrentio/index.html.twig</code></li>
</ul>
</div>
{% endblock %}

View File

@@ -1,13 +1,4 @@
<div class="p-4 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
{% if results.file != false %}
<div class="p-3 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
<p class="font-bold text-sm text-left">Existing file(s) for this movie:</p>
<ul class="list-disc ml-3 overflow-scroll">
<li class="font-normal">{{ results.file.realPath|strip_media_path }} &mdash; <strong>{{ results.file.size|filesize }}</strong></li>
</ul>
</div>
{% endif %}
<div class="overflow-hidden rounded-md">
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
</div>