Compare commits
2 Commits
dev-php85-
...
dev-config
| Author | SHA1 | Date | |
|---|---|---|---|
| 14739500f4 | |||
| 0aee4ae7df |
8
.env
8
.env
@@ -51,3 +51,11 @@ OIDC_CLIENT_ID="Enter your OIDC client id"
|
|||||||
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
|
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
|
||||||
OIDC_BYPASS_FORM_LOGIN=false
|
OIDC_BYPASS_FORM_LOGIN=false
|
||||||
###< drenso/symfony-oidc-bundle ###
|
###< drenso/symfony-oidc-bundle ###
|
||||||
|
|
||||||
|
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_FROM=
|
||||||
|
SMTP_FROM_NAME=""
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,4 +18,3 @@ bolt.db
|
|||||||
###> phpstan/phpstan ###
|
###> phpstan/phpstan ###
|
||||||
phpstan.neon
|
phpstan.neon
|
||||||
###< phpstan/phpstan ###
|
###< phpstan/phpstan ###
|
||||||
.php-cs-fixer.cache
|
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,17 +1,16 @@
|
|||||||
FROM registry.caldwell.digital/home/frankenphp:1.9.0-php8.5.0-ubuntu
|
FROM dunglas/frankenphp:php8.4
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ENV SERVER_NAME=":80"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
ENV APP_VERSION="0.0.1"
|
ENV APP_VERSION="0.0.1"
|
||||||
ENV SERVER_ROOT="/app/public"
|
|
||||||
|
|
||||||
#RUN install-php-extensions \
|
RUN install-php-extensions \
|
||||||
# pdo_mysql \
|
pdo_mysql \
|
||||||
# gd \
|
gd \
|
||||||
# intl \
|
intl \
|
||||||
# zip \
|
zip \
|
||||||
# opcache
|
opcache
|
||||||
|
|
||||||
RUN apt update && apt install -y wget
|
RUN apt update && apt install -y wget
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import './bootstrap.js';
|
|||||||
* which should already be in your base.html.twig.
|
* which should already be in your base.html.twig.
|
||||||
*/
|
*/
|
||||||
import './styles/app.css';
|
import './styles/app.css';
|
||||||
import PullToRefresh from 'pulltorefreshjs';
|
|
||||||
|
|
||||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||||
|
|
||||||
@@ -19,10 +18,3 @@ var observer = new MutationObserver(function(mutations) {
|
|||||||
|
|
||||||
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
||||||
|
|
||||||
const ptr = PullToRefresh.init({
|
|
||||||
mainElement: 'body',
|
|
||||||
onRefresh() {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getComponent } from '@symfony/ux-live-component';
|
|||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['download', 'deleteFileInput']
|
static targets = ['download']
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this.component = await getComponent(this.element);
|
this.component = await getComponent(this.element);
|
||||||
@@ -42,8 +42,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteDownload(data) {
|
deleteDownload(data) {
|
||||||
const deleteFileInput = document.querySelector(`#delete_file_${data.params.id}`)
|
fetch(`/api/download/${data.params.id}`, {method: 'DELETE'})
|
||||||
fetch(`/api/download/${data.params.id}?deleteFile=${deleteFileInput.checked}`, {method: 'DELETE'})
|
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(json => console.debug(json));
|
.then(json => console.debug(json));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,19 +20,24 @@ export default class extends Controller {
|
|||||||
|
|
||||||
options = []
|
options = []
|
||||||
optionsLoaded = false
|
optionsLoaded = false
|
||||||
resultCountEl = null
|
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this.resultCountEl = document.querySelector('#movie_results_count');
|
await this.setOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTargetConnected() {
|
async setOptions() {
|
||||||
this.optionsLoaded = true;
|
if (false === this.optionsLoaded) {
|
||||||
this.options = this.element.querySelectorAll('tbody tr');
|
this.optionsLoaded = true;
|
||||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
await fetch(`/torrentio/movies/${this.tmdbIdValue}/${this.imdbIdValue}`)
|
||||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
.then(res => res.text())
|
||||||
this.loadingIconOutlet.toggleIcon();
|
.then(response => {
|
||||||
this.resultCountEl.innerText = this.options.length;
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeps compatible with Filter & TV Shows
|
// Keeps compatible with Filter & TV Shows
|
||||||
@@ -97,6 +102,5 @@ export default class extends Controller {
|
|||||||
count = count + 1;
|
count = count + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.resultCountEl.innerText = count;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,18 +25,37 @@ export default class extends Controller {
|
|||||||
optionsLoaded = false
|
optionsLoaded = false
|
||||||
isOpen = false
|
isOpen = false
|
||||||
|
|
||||||
async listTargetConnected() {
|
async connect() {
|
||||||
this.options = this.element.querySelectorAll('tbody tr');
|
await this.setOptions();
|
||||||
if (this.options.length > 0) {
|
}
|
||||||
this.options.forEach((option) =>
|
|
||||||
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
async setOptions() {
|
||||||
);
|
if (this.optionsLoaded === false) {
|
||||||
this.options[0].querySelector('input[type="checkbox"]').checked = true;
|
this.optionsLoaded = true;
|
||||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
let response;
|
||||||
this.loadingIconOutlet.increaseCount();
|
|
||||||
} else {
|
try {
|
||||||
this.countTarget.innerText = 0;
|
response = await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||||
this.episodeSelectorTarget.disabled = true;
|
} 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}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +134,6 @@ export default class extends Controller {
|
|||||||
"codec": option.querySelector('#codec').textContent.trim(),
|
"codec": option.querySelector('#codec').textContent.trim(),
|
||||||
"provider": option.querySelector('#provider').textContent.trim(),
|
"provider": option.querySelector('#provider').textContent.trim(),
|
||||||
"languages": JSON.parse(option.dataset['languages']),
|
"languages": JSON.parse(option.dataset['languages']),
|
||||||
"quality": option.dataset['quality'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let include = true;
|
let include = true;
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ services:
|
|||||||
- mercure_data:/data
|
- mercure_data:/data
|
||||||
- mercure_config:/config
|
- mercure_config:/config
|
||||||
tty: true
|
tty: true
|
||||||
ports:
|
|
||||||
- "8001:80"
|
|
||||||
command: "frankenphp php-server --root=/app/public"
|
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"symfony/mailer": "7.3.*",
|
"symfony/mailer": "7.3.*",
|
||||||
"symfony/mercure-bundle": "^0.3.9",
|
"symfony/mercure-bundle": "^0.3.9",
|
||||||
"symfony/messenger": "7.3.*",
|
"symfony/messenger": "7.3.*",
|
||||||
|
"symfony/monolog-bundle": "^3.10",
|
||||||
"symfony/runtime": "7.3.*",
|
"symfony/runtime": "7.3.*",
|
||||||
"symfony/scheduler": "7.3.*",
|
"symfony/scheduler": "7.3.*",
|
||||||
"symfony/security-bundle": "7.3.*",
|
"symfony/security-bundle": "7.3.*",
|
||||||
|
|||||||
264
composer.lock
generated
264
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0f98dada0a01d471cebf4eb1b51b9006",
|
"content-hash": "cf9a5b3cdb6e21e270da6c4efee09005",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -2681,6 +2681,109 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-02-06T08:48:15+00:00"
|
"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",
|
"name": "nesbot/carbon",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@@ -7578,6 +7681,165 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-02-19T08:51:26+00:00"
|
"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",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ 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],
|
||||||
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
78
config/packages/monolog.yaml
Normal file
78
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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]
|
||||||
@@ -6,14 +6,6 @@ controllersBase:
|
|||||||
defaults:
|
defaults:
|
||||||
schemes: [ 'https' ]
|
schemes: [ 'https' ]
|
||||||
|
|
||||||
controllersLibrary:
|
|
||||||
resource:
|
|
||||||
path: ../src/Library/Framework/Controller/
|
|
||||||
namespace: App\Library\Framework\Controller
|
|
||||||
type: attribute
|
|
||||||
defaults:
|
|
||||||
schemes: [ 'https' ]
|
|
||||||
|
|
||||||
controllersSearch:
|
controllersSearch:
|
||||||
resource:
|
resource:
|
||||||
path: ../src/Search/Framework/Controller/
|
path: ../src/Search/Framework/Controller/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
parameters:
|
parameters:
|
||||||
# App
|
# App
|
||||||
app.url: '%env(APP_URL)%'
|
app.url: '%env(APP_URL)%'
|
||||||
|
app.env: '%env(default:app.default.env:APP_ENV)%'
|
||||||
app.version: '%env(default:app.default.version:APP_VERSION)%'
|
app.version: '%env(default:app.default.version:APP_VERSION)%'
|
||||||
|
|
||||||
# Debrid Services
|
# Debrid Services
|
||||||
@@ -32,6 +33,7 @@ parameters:
|
|||||||
app.cache.redis.host.default: 'redis://redis'
|
app.cache.redis.host.default: 'redis://redis'
|
||||||
|
|
||||||
# Various configs
|
# Various configs
|
||||||
|
app.default.env: 'prod'
|
||||||
app.default.version: '0.dev'
|
app.default.version: '0.dev'
|
||||||
app.default.timezone: 'America/Chicago'
|
app.default.timezone: 'America/Chicago'
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
frankenphp {
|
frankenphp {
|
||||||
{$FRANKENPHP_CONFIG}
|
{$FRANKENPHP_CONFIG}
|
||||||
num_threads 10
|
|
||||||
max_threads 20
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,4 @@ return [
|
|||||||
'version' => '2.4.3',
|
'version' => '2.4.3',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
],
|
],
|
||||||
'pulltorefreshjs' => [
|
|
||||||
'version' => '0.1.22',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once '../vendor/autoload.php';
|
|
||||||
|
|
||||||
use App\Torrentio\Result\ResultFactory;
|
|
||||||
|
|
||||||
$realDebridKey = "";
|
|
||||||
$tasks = [];
|
|
||||||
$results = [];
|
|
||||||
$start = microtime(true);
|
|
||||||
for ($i = 1; $i <= 20; $i++) {
|
|
||||||
$tasks[] = \Async\spawn(function () use ($i, &$results, &$realDebridKey) {
|
|
||||||
$baseUrl = "https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={$realDebridKey}/stream/movie/tt0412142:1:$i.json";
|
|
||||||
$options = \json_decode(file_get_contents($baseUrl), true);
|
|
||||||
|
|
||||||
foreach ($options['streams'] as $stream) {
|
|
||||||
if (!str_starts_with($stream['url'], "https")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
array_key_exists('behaviorHints', $stream) &&
|
|
||||||
array_key_exists('bingeGroup', $stream['behaviorHints'])
|
|
||||||
) {
|
|
||||||
$bingeGroup = $stream['behaviorHints']['bingeGroup'];
|
|
||||||
} else {
|
|
||||||
$bingeGroup = '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = ResultFactory::map(
|
|
||||||
$stream['url'],
|
|
||||||
$stream['title'],
|
|
||||||
$bingeGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
$results[] = $result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
\Async\awaitAll($tasks);
|
|
||||||
$end = microtime(true) - $start;
|
|
||||||
dd($end, $results);
|
|
||||||
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//// Spawn multiple concurrent coroutines
|
|
||||||
//Async\spawn(function() {
|
|
||||||
// echo "Starting coroutine 1\n";
|
|
||||||
// sleep(2); // Non-blocking in async context
|
|
||||||
// echo "Coroutine 1 completed\n";
|
|
||||||
//});
|
|
||||||
//
|
|
||||||
//Async\spawn(function() {
|
|
||||||
// echo "Starting coroutine 2\n";
|
|
||||||
// sleep(1); // Non-blocking in async context
|
|
||||||
// echo "Coroutine 2 completed\n";
|
|
||||||
//});
|
|
||||||
//
|
|
||||||
//echo "All coroutines started\n";
|
|
||||||
|
|
||||||
105
src/Base/Config/AppConfig.php
Normal file
105
src/Base/Config/AppConfig.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/Base/Config/Auth/OidcConfig.php
Normal file
88
src/Base/Config/Auth/OidcConfig.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Base/Config/AuthConfig.php
Normal file
62
src/Base/Config/AuthConfig.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Base/Config/ConfigInterface.php
Normal file
12
src/Base/Config/ConfigInterface.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
113
src/Base/Config/SmtpConfig.php
Normal file
113
src/Base/Config/SmtpConfig.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,58 +2,60 @@
|
|||||||
|
|
||||||
namespace App\Base;
|
namespace App\Base;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use App\Base\Config\AppConfig;
|
||||||
|
use App\Base\Config\AuthConfig;
|
||||||
|
use App\Base\Config\SmtpConfig;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||||
|
|
||||||
final class ConfigResolver
|
final class ConfigResolver
|
||||||
{
|
{
|
||||||
private array $messages = [];
|
private array $messages = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire(param: 'app.url')]
|
private readonly LoggerInterface $logger,
|
||||||
private readonly ?string $appUrl = null,
|
private readonly TagAwareCacheInterface $cache,
|
||||||
|
private readonly AuthConfig $authConfig,
|
||||||
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
private readonly SmtpConfig $smtpConfig,
|
||||||
private readonly ?string $realDebridApiKey = null,
|
private readonly AppConfig $appConfig,
|
||||||
|
|
||||||
#[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
|
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;
|
$valid = true;
|
||||||
|
|
||||||
if (null === $this->realDebridApiKey || "" === $this->realDebridApiKey) {
|
if (false === $this->appConfig->isValid()) {
|
||||||
$this->messages[] = "Your Real Debrid API key is missing. Please set it to the 'REAL_DEBRID_KEY' environment variable.";
|
$this->messages += $this->appConfig->getMessages();
|
||||||
$valid = false;
|
$valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $this->tmdbApiKey || "" === $this->tmdbApiKey) {
|
if (false === $this->authConfig->isValid()) {
|
||||||
$this->messages[] = "Your TMDB API key is missing. Please set it to the 'TMDB_API' environment variable.";
|
$this->messages += $this->authConfig->getMessages();
|
||||||
$valid = false;
|
$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;
|
return $valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,34 +64,18 @@ final class ConfigResolver
|
|||||||
return $this->messages;
|
return $this->messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAuthConfig(): AuthConfig
|
||||||
|
{
|
||||||
|
return $this->authConfig;
|
||||||
|
}
|
||||||
|
|
||||||
public function authIs(string $method): bool
|
public function authIs(string $method): bool
|
||||||
{
|
{
|
||||||
if (strtolower($method) === strtolower($this->getAuthMethod())) {
|
return $this->authConfig->isMethod($method);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAuthMethod(): string
|
public function getAuthMethod(): string
|
||||||
{
|
{
|
||||||
return strtolower($this->authMethod);
|
return $this->authConfig->getAuthMethod();
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ readonly class Broadcaster
|
|||||||
private Environment $renderer,
|
private Environment $renderer,
|
||||||
private HubInterface $hub,
|
private HubInterface $hub,
|
||||||
private RequestStack $requestStack,
|
private RequestStack $requestStack,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public function alert(string $title, string $message, string $type = "success"): void
|
public function alert(string $title, string $message, string $type = "success"): void
|
||||||
{
|
{
|
||||||
@@ -31,4 +32,18 @@ readonly class Broadcaster
|
|||||||
);
|
);
|
||||||
$this->hub->publish($update);
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Base\Service;
|
|||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use Nihilarr\PTN;
|
use Nihilarr\PTN;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Symfony\Component\Finder\Finder;
|
use Symfony\Component\Finder\Finder;
|
||||||
@@ -22,7 +21,6 @@ class MediaFiles
|
|||||||
private string $tvShowsPath;
|
private string $tvShowsPath;
|
||||||
|
|
||||||
private Filesystem $filesystem;
|
private Filesystem $filesystem;
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire(param: 'media.base_path')]
|
#[Autowire(param: 'media.base_path')]
|
||||||
@@ -35,14 +33,12 @@ class MediaFiles
|
|||||||
string $tvShowsPath,
|
string $tvShowsPath,
|
||||||
|
|
||||||
Filesystem $filesystem,
|
Filesystem $filesystem,
|
||||||
LoggerInterface $logger,
|
|
||||||
) {
|
) {
|
||||||
$this->finder = new Finder();
|
$this->finder = new Finder();
|
||||||
$this->basePath = $basePath;
|
$this->basePath = $basePath;
|
||||||
$this->moviesPath = $moviesPath;
|
$this->moviesPath = $moviesPath;
|
||||||
$this->tvShowsPath = $tvShowsPath;
|
$this->tvShowsPath = $tvShowsPath;
|
||||||
$this->filesystem = $filesystem;
|
$this->filesystem = $filesystem;
|
||||||
$this->logger = $logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPathByType(string $mediaType): string
|
public function getPathByType(string $mediaType): string
|
||||||
@@ -144,7 +140,7 @@ class MediaFiles
|
|||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber): SplFileInfo|false
|
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber)
|
||||||
{
|
{
|
||||||
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||||
|
|
||||||
@@ -224,26 +220,4 @@ class MediaFiles
|
|||||||
{
|
{
|
||||||
$this->filesystem->chmod($filepath, $permissions);
|
$this->filesystem->chmod($filepath, $permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $filepath
|
|
||||||
* @return bool
|
|
||||||
* Returns true if file was deleted
|
|
||||||
* Returns false is file not found or was not deleted
|
|
||||||
*/
|
|
||||||
public function removeFile(string $filepath): bool
|
|
||||||
{
|
|
||||||
if (true === $this->filesystem->exists($filepath)) {
|
|
||||||
try {
|
|
||||||
$this->filesystem->remove($filepath);
|
|
||||||
return true;
|
|
||||||
} catch (\Throwable $exception) {
|
|
||||||
$this->logger->error($exception->getMessage(), ['file' => $filepath]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->warning('> [MediaFiles] Attempted to remove file, but it did not exist.', ['file' => $filepath]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ class DeleteDownloadCommand implements CommandInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public int $downloadId,
|
public int $downloadId,
|
||||||
public bool $deleteFile = false,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,6 @@ namespace App\Download\Action\Handler;
|
|||||||
use App\Download\Action\Command\DeleteDownloadCommand;
|
use App\Download\Action\Command\DeleteDownloadCommand;
|
||||||
use App\Download\Action\Result\DeleteDownloadResult;
|
use App\Download\Action\Result\DeleteDownloadResult;
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Library\Action\Command\DeleteMediaFileCommand;
|
|
||||||
use App\Library\Action\Handler\DeleteMediaFileHandler;
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
@@ -16,26 +14,13 @@ readonly class DeleteDownloadHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DownloadRepository $downloadRepository,
|
private DownloadRepository $downloadRepository,
|
||||||
private DeleteMediaFileHandler $deleteMediaFileHandler,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$download = $this->downloadRepository->find($command->downloadId);
|
$download = $this->downloadRepository->find($command->downloadId);
|
||||||
|
|
||||||
if (true === $command->deleteFile) {
|
|
||||||
$deletedFileResult = $this->deleteMediaFileHandler->handle(new DeleteMediaFileCommand(
|
|
||||||
filename: $download->getFilename(),
|
|
||||||
downloadId: $command->downloadId
|
|
||||||
));
|
|
||||||
}
|
|
||||||
$this->downloadRepository->delete($command->downloadId);
|
$this->downloadRepository->delete($command->downloadId);
|
||||||
|
|
||||||
return new DeleteDownloadResult(
|
return new DeleteDownloadResult(200, 'Success', $download);
|
||||||
status: 200,
|
|
||||||
message: 'Success',
|
|
||||||
download: $download,
|
|
||||||
deleteMediaFileResult: $deletedFileResult ?? null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Download\Action\Input;
|
namespace App\Download\Action\Input;
|
||||||
|
|
||||||
use App\Download\Action\Command\DeleteDownloadCommand;
|
use App\Download\Action\Command\DeleteDownloadCommand;
|
||||||
use OneToMany\RichBundle\Attribute\SourceQuery;
|
|
||||||
use OneToMany\RichBundle\Attribute\SourceRoute;
|
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\InputInterface;
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
@@ -14,15 +13,12 @@ class DeleteDownloadInput implements InputInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[SourceRoute('downloadId')]
|
#[SourceRoute('downloadId')]
|
||||||
public int $downloadId,
|
public int $downloadId,
|
||||||
#[SourceQuery('deleteFile')]
|
|
||||||
public bool $deleteFile = false,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function toCommand(): CommandInterface
|
public function toCommand(): CommandInterface
|
||||||
{
|
{
|
||||||
return new DeleteDownloadCommand(
|
return new DeleteDownloadCommand(
|
||||||
$this->downloadId,
|
$this->downloadId,
|
||||||
$this->deleteFile,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,5 @@ class DeleteDownloadResult implements ResultInterface
|
|||||||
public int $status,
|
public int $status,
|
||||||
public string $message,
|
public string $message,
|
||||||
public Download $download,
|
public Download $download,
|
||||||
public ?DeleteMediaFileResult $deleteMediaFileResult = null,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Download\Action\Result;
|
|
||||||
|
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
|
||||||
|
|
||||||
class DeleteMediaFileResult implements ResultInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $message,
|
|
||||||
public string $filepath,
|
|
||||||
public bool $isDeleted,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -66,7 +66,7 @@ class ApiController extends AbstractController
|
|||||||
message: "{$result->download->getTitle()} has been deleted.",
|
message: "{$result->download->getTitle()} has been deleted.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->json($result);
|
return $this->json(['status' => 200, 'message' => 'Download Deleted']);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/download/{downloadId}/pause', name: 'api_download_pause', methods: ['PATCH'])]
|
#[Route('/api/download/{downloadId}/pause', name: 'api_download_pause', methods: ['PATCH'])]
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\User\Framework\Entity\User;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
||||||
use Nihilarr\PTN;
|
use Nihilarr\PTN;
|
||||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
|
||||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
|
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
|
||||||
@@ -45,7 +44,6 @@ class Download
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $episodeId = null;
|
private ?string $episodeId = null;
|
||||||
|
|
||||||
#[Ignore]
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'downloads')]
|
#[ORM\ManyToOne(inversedBy: 'downloads')]
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Library\Action\Command;
|
|
||||||
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements CommandInterface<DeleteMediaFileCommand>
|
|
||||||
*/
|
|
||||||
class DeleteMediaFileCommand implements CommandInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $filename,
|
|
||||||
public ?int $downloadId = null,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?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,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Library\Action\Handler;
|
|
||||||
|
|
||||||
use App\Base\Service\MediaFiles;
|
|
||||||
use App\Download\Action\Result\DeleteMediaFileResult;
|
|
||||||
use App\Download\Framework\Entity\Download;
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
|
||||||
use App\Library\Action\Command\DeleteMediaFileCommand;
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements HandlerInterface<DeleteMediaFileCommand,DeleteMediaFileResult>
|
|
||||||
*/
|
|
||||||
class DeleteMediaFileHandler implements HandlerInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly DownloadRepository $downloadRepository,
|
|
||||||
private readonly MediaFiles $mediaFiles,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
|
||||||
{
|
|
||||||
/** @var Download $downloadRecord */
|
|
||||||
$downloadRecord = $this->downloadRepository->find($command->downloadId);
|
|
||||||
$filepath = $this->getFullFilepath($downloadRecord);
|
|
||||||
$result = $this->mediaFiles->removeFile($filepath);
|
|
||||||
|
|
||||||
return new DeleteMediaFileResult(
|
|
||||||
message: true === $result ? 'File removed' : 'File not removed',
|
|
||||||
filepath: $filepath,
|
|
||||||
isDeleted: $result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getFullFilepath(Download $download): string
|
|
||||||
{
|
|
||||||
return $this->mediaFiles->getPathByType($download->getMediaType()) . DIRECTORY_SEPARATOR . $download->getFilename();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Library\Action\Input;
|
|
||||||
|
|
||||||
use App\Library\Action\Command\DeleteMediaFileCommand;
|
|
||||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
|
||||||
use OneToMany\RichBundle\Contract\InputInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements InputInterface<DeleteMediaFileInput,DeleteMediaFileCommand>
|
|
||||||
*/
|
|
||||||
class DeleteMediaFileInput implements InputInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
#[SourceRequest('filename')]
|
|
||||||
public string $filename,
|
|
||||||
#[SourceRequest('downloadId', nullify: true)]
|
|
||||||
public ?int $downloadId = null,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function toCommand(): CommandInterface
|
|
||||||
{
|
|
||||||
return new DeleteMediaFileCommand(
|
|
||||||
$this->filename,
|
|
||||||
$this->downloadId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Library\Action\Result;
|
|
||||||
|
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ResultInterface
|
|
||||||
*/
|
|
||||||
class DeleteMediaFileResult implements ResultInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $status,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?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,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?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|false $fileInfo): self|false
|
|
||||||
{
|
|
||||||
return false === $fileInfo ? false : new static(
|
|
||||||
path: $fileInfo->getRealPath(),
|
|
||||||
filename: $fileInfo->getFilename(),
|
|
||||||
extension: $fileInfo->getExtension(),
|
|
||||||
size: $fileInfo->getSize(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?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')
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?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,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ namespace App\Monitor\Action\Handler;
|
|||||||
use App\Base\Util\EpisodeId;
|
use App\Base\Util\EpisodeId;
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Download\DownloadOptionEvaluator;
|
use App\Download\DownloadOptionEvaluator;
|
||||||
use App\Download\Framework\Entity\Download;
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
@@ -16,7 +15,6 @@ use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
|||||||
use App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
@@ -29,7 +27,6 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||||
private EntityManagerInterface $entityManager,
|
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private MonitorRepository $monitorRepository,
|
private MonitorRepository $monitorRepository,
|
||||||
|
|||||||
@@ -18,19 +18,22 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
|||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||||
|
|
||||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||||
readonly class MonitorTvShowHandler implements HandlerInterface
|
readonly class MonitorTvShowHandler implements HandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[Target('monitorLogger')]
|
||||||
|
private LoggerInterface $logger,
|
||||||
private MonitorRepository $monitorRepository,
|
private MonitorRepository $monitorRepository,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private LoggerInterface $logger,
|
|
||||||
private Tmdb $tmdb,
|
private Tmdb $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Monitor\Action\Command\MonitorTvShowCommand;
|
|||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
||||||
|
|
||||||
@@ -16,13 +17,14 @@ use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
|||||||
class MonitorDispatcher
|
class MonitorDispatcher
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[Target('monitorLogger')]
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly MonitorRepository $monitorRepository,
|
private readonly MonitorRepository $monitorRepository,
|
||||||
private readonly MessageBusInterface $bus,
|
private readonly MessageBusInterface $bus,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke() {
|
public function __invoke() {
|
||||||
$this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher');
|
$this->logger->info('[MonitorDispatcher] > Executing MonitorDispatcher');
|
||||||
|
|
||||||
$this->cleanupStuckMonitors();
|
$this->cleanupStuckMonitors();
|
||||||
|
|
||||||
@@ -34,15 +36,19 @@ class MonitorDispatcher
|
|||||||
];
|
];
|
||||||
|
|
||||||
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
||||||
|
$this->logger->info('[MonitorDispatcher] ' . count($monitors) . ' monitors found');
|
||||||
|
|
||||||
foreach ($monitors as $monitor) {
|
foreach ($monitors as $monitor) {
|
||||||
|
$this->logger->info('[MonitorDispatcher] - Evaluating monitor ' . $monitor->getId() . ' for "' . $monitor->getTitle() . '"');
|
||||||
$monitor->setStatus('In Progress');
|
$monitor->setStatus('In Progress');
|
||||||
$this->monitorRepository->getEntityManager()->flush();
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
$command = $monitorHandlers[$monitor->getMonitorType()];
|
$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->bus->dispatch(new $command($monitor->getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->logger->info('[MonitorDispatcher] < Complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanupStuckMonitors(): void
|
private function cleanupStuckMonitors(): void
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Handler;
|
namespace App\Search\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Search\Action\Command\GetMediaInfoCommand;
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
use App\Search\Action\Result\GetMediaInfoResult;
|
use App\Search\Action\Result\GetMediaInfoResult;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
@@ -14,12 +15,19 @@ class GetMediaInfoHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Tmdb $tmdb,
|
private readonly Tmdb $tmdb,
|
||||||
|
private readonly MediaFiles $mediaFiles
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$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);
|
return new GetMediaInfoResult($media, $command->season);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Search\Framework\Command;
|
|
||||||
|
|
||||||
use Aimeos\Map;
|
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
|
||||||
use App\Download\Action\Handler\DownloadMediaHandler;
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
|
||||||
use App\Search\Action\Handler\SearchHandler;
|
|
||||||
use App\Search\Action\Command\SearchCommand as CommandInput;
|
|
||||||
use App\Search\Action\Result\SearchResult;
|
|
||||||
use App\Tmdb\TmdbResult;
|
|
||||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
|
||||||
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
|
||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
|
||||||
use App\Torrentio\Result\TorrentioResult;
|
|
||||||
use App\User\Framework\Repository\UserRepository;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Helper\Table;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
|
||||||
use Symfony\Component\Console\Question\Question;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
|
||||||
|
|
||||||
#[AsCommand('search')]
|
|
||||||
class SearchCommand extends Command
|
|
||||||
{
|
|
||||||
private SearchHandler $searchHandler;
|
|
||||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler;
|
|
||||||
private GetMovieOptionsHandler $getMovieOptionsHandler;
|
|
||||||
private UserRepository $userRepository;
|
|
||||||
private DownloadRepository $downloadRepository;
|
|
||||||
private DownloadMediaHandler $downloadMediaHandler;
|
|
||||||
private MessageBusInterface $bus;
|
|
||||||
|
|
||||||
public function __construct(SearchHandler $searchHandler, GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
|
||||||
GetMovieOptionsHandler $getMovieOptionsHandler,
|
|
||||||
UserRepository $userRepository,
|
|
||||||
DownloadRepository $downloadRepository,
|
|
||||||
DownloadMediaHandler $downloadMediaHandler,
|
|
||||||
MessageBusInterface $bus,
|
|
||||||
?string $name = null
|
|
||||||
) {
|
|
||||||
parent::__construct($name);
|
|
||||||
$this->searchHandler = $searchHandler;
|
|
||||||
$this->getTvShowOptionsHandler = $getTvShowOptionsHandler;
|
|
||||||
$this->getMovieOptionsHandler = $getMovieOptionsHandler;
|
|
||||||
$this->userRepository = $userRepository;
|
|
||||||
$this->downloadRepository = $downloadRepository;
|
|
||||||
$this->downloadMediaHandler = $downloadMediaHandler;
|
|
||||||
$this->bus = $bus;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure()
|
|
||||||
{
|
|
||||||
$this->addArgument('term', InputArgument::REQUIRED);
|
|
||||||
$this->addOption('local', 'l', InputOption::VALUE_NONE, 'Perform the download locally instead of submitting it to the queue.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$command = new CommandInput($input->getArgument('term'));
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
$mediaOptions = $this->searchHandler->handle($command);
|
|
||||||
|
|
||||||
// Render results and ask the User to pick one
|
|
||||||
$mediaChoice = $this->askToChooseMediaOption($io, $output, $mediaOptions);
|
|
||||||
|
|
||||||
// Find download options based on the User's choice
|
|
||||||
$downloadOptions = $this->fetchDownloadOptions($mediaChoice);
|
|
||||||
|
|
||||||
// Render results and ask the User to pick one
|
|
||||||
$downloadChoice = $this->askToChooseDownloadOption($io, $output, $downloadOptions);
|
|
||||||
|
|
||||||
// Have user confirm download action
|
|
||||||
$confirmation = $this->askToConfirmDownload($io, $output, $downloadChoice);
|
|
||||||
|
|
||||||
// Begin download or submit to the queue
|
|
||||||
if (true === $confirmation) {
|
|
||||||
$downloadLocally = $input->getOption('local');
|
|
||||||
$this->submitDownload($io, $mediaChoice, $downloadChoice, $downloadLocally);
|
|
||||||
} else {
|
|
||||||
$io->success('No results found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->success('Success!');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function askToChooseMediaOption(SymfonyStyle $io, OutputInterface $output, SearchResult $result): TmdbResult
|
|
||||||
{
|
|
||||||
$table = new Table($output)
|
|
||||||
->setHeaders(['ID', 'Title', 'Year', 'IMDb ID', 'Description'])
|
|
||||||
->setRows(
|
|
||||||
Map::from($result->results)
|
|
||||||
->map(fn ($result, $index) => [$index, $result->title, $result->year, $result->imdbId, substr($result->description, 0, 80) . '...'])
|
|
||||||
->toArray()
|
|
||||||
);
|
|
||||||
|
|
||||||
$table->render();
|
|
||||||
|
|
||||||
$question = new Question('Enter the ID of the correct result: ');
|
|
||||||
$choiceId = $io->askQuestion($question);
|
|
||||||
$choice = $result->results[$choiceId];
|
|
||||||
|
|
||||||
$io->info('You chose: ' . $choice->title);
|
|
||||||
$io->table(
|
|
||||||
['ID', 'Title', 'Year', 'IMDb ID', 'Description'],
|
|
||||||
[
|
|
||||||
[$choiceId, $choice->title, $choice->year, $choice->imdbId, substr($choice->description, 0, 80) . '...'],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $choice;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchDownloadOptions(TmdbResult $result): array
|
|
||||||
{
|
|
||||||
$handlers = [
|
|
||||||
'movies' => $this->getMovieOptionsHandler,
|
|
||||||
'tvshows' => $this->getTvShowOptionsHandler,
|
|
||||||
];
|
|
||||||
|
|
||||||
$handler = $handlers[$result->mediaType];
|
|
||||||
|
|
||||||
if ("movies" === $result->mediaType) {
|
|
||||||
$command = new GetMovieOptionsCommand(
|
|
||||||
$result->tmdbId,
|
|
||||||
$result->imdbId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$command = new GetTvShowOptionsCommand(
|
|
||||||
$result->tmdbId,
|
|
||||||
$result->imdbId,
|
|
||||||
1,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $handler->handle($command);
|
|
||||||
return $result->results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function askToChooseDownloadOption(SymfonyStyle $io, OutputInterface $output, array $options): TorrentioResult
|
|
||||||
{
|
|
||||||
$table = new Table($output)
|
|
||||||
->setHeaders(['ID', 'Size', 'Resolution', 'Codec', 'Seeders', 'Provider', 'Language'])
|
|
||||||
->setRows(
|
|
||||||
Map::from($options)
|
|
||||||
->map(fn (TorrentioResult $result, $index) => [$index, $result->size, $result->resolution, $result->codec, $result->seeders, $result->provider, implode(', ', $result->languages)])
|
|
||||||
->toArray()
|
|
||||||
);
|
|
||||||
$table->render();
|
|
||||||
|
|
||||||
$question = new Question('Enter the ID of the item to download: ');
|
|
||||||
$choiceId = $io->askQuestion($question);
|
|
||||||
$choice = $options[$choiceId];
|
|
||||||
$io->info('You chose: ' . $choice->title);
|
|
||||||
|
|
||||||
return $choice;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function askToConfirmDownload(SymfonyStyle $io, OutputInterface $output, TorrentioResult $downloadOption): bool
|
|
||||||
{
|
|
||||||
$question = new ChoiceQuestion('Are you sure you want to download the above file?', ['yes', 'no']);
|
|
||||||
$choice = $io->askQuestion($question);
|
|
||||||
return $choice === 'yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function submitDownload(SymfonyStyle $io, TmdbResult $mediaChoice, TorrentioResult $downloadOption, bool $downloadLocally = false): void
|
|
||||||
{
|
|
||||||
$io->writeln('> Adding download record');
|
|
||||||
$user = $this->userRepository->find(1);
|
|
||||||
$download = $this->downloadRepository->insert(
|
|
||||||
$user,
|
|
||||||
$downloadOption->url,
|
|
||||||
$downloadOption->title,
|
|
||||||
$downloadOption->filename,
|
|
||||||
$mediaChoice->imdbId,
|
|
||||||
$mediaChoice->mediaType,
|
|
||||||
);
|
|
||||||
|
|
||||||
$io->writeln('> Download record added: ' . $download->getId());
|
|
||||||
$downloadCommand = new DownloadMediaCommand(
|
|
||||||
$download->getUrl(),
|
|
||||||
$download->getTitle(),
|
|
||||||
$download->getFilename(),
|
|
||||||
$download->getMEdiaType(),
|
|
||||||
$download->getImdbId(),
|
|
||||||
$download->getUser()->getId(),
|
|
||||||
$download->getId()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (true === $downloadLocally) {
|
|
||||||
$io->writeln('> Beginning local download');
|
|
||||||
$this->downloadMediaHandler->handle($downloadCommand);
|
|
||||||
} else {
|
|
||||||
$io->writeln('> Submitting download to queue');
|
|
||||||
$this->bus->dispatch($downloadCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->writeln('> Download added to queue');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,7 @@ final class WebController extends AbstractController
|
|||||||
public function result(
|
public function result(
|
||||||
GetMediaInfoInput $input,
|
GetMediaInfoInput $input,
|
||||||
?int $season = null,
|
?int $season = null,
|
||||||
): Response
|
): Response {
|
||||||
{
|
|
||||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||||
|
|
||||||
return $this->render('search/result.html.twig', [
|
return $this->render('search/result.html.twig', [
|
||||||
@@ -53,4 +52,32 @@ 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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Tmdb;
|
namespace App\Tmdb;
|
||||||
|
|
||||||
use \Async;
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Base\Enum\MediaType;
|
use App\Base\Enum\MediaType;
|
||||||
use App\ValueObject\ResultFactory;
|
use App\ValueObject\ResultFactory;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Contracts\Cache\ItemInterface;
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
@@ -44,7 +42,6 @@ class Tmdb
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
private readonly LoggerInterface $logger,
|
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
) {
|
) {
|
||||||
$this->client = new Client(
|
$this->client = new Client(
|
||||||
@@ -217,6 +214,7 @@ class Tmdb
|
|||||||
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
|
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$series['episodes'][$season['season_number']] = Map::from(
|
$series['episodes'][$season['season_number']] = Map::from(
|
||||||
$client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
|
$client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
|
||||||
)->map(function ($data) {
|
)->map(function ($data) {
|
||||||
@@ -303,7 +301,6 @@ class Tmdb
|
|||||||
description: $data['overview'],
|
description: $data['overview'],
|
||||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||||
mediaType: "movies",
|
mediaType: "movies",
|
||||||
episodeAirDate: (new \DateTime($data['release_date']))->format('m/d/Y'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Torrentio\Action\Handler;
|
namespace App\Torrentio\Action\Handler;
|
||||||
|
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Library\Dto\MediaFileDto;
|
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
@@ -29,7 +28,7 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
|||||||
|
|
||||||
return new GetTvShowOptionsResult(
|
return new GetTvShowOptionsResult(
|
||||||
media: $media,
|
media: $media,
|
||||||
file: MediaFileDto::fromSplFileInfo($file),
|
file: $file,
|
||||||
season: $command->season,
|
season: $command->season,
|
||||||
episode: $command->episode,
|
episode: $command->episode,
|
||||||
results: $this->torrentio->fetchEpisodeResults(
|
results: $this->torrentio->fetchEpisodeResults(
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Torrentio\Action\Result;
|
namespace App\Torrentio\Action\Result;
|
||||||
|
|
||||||
use App\Library\Dto\MediaFileDto;
|
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
use Symfony\Component\Finder\SplFileInfo;
|
||||||
|
|
||||||
class GetTvShowOptionsResult implements ResultInterface
|
class GetTvShowOptionsResult implements ResultInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $media,
|
public TmdbResult $media,
|
||||||
public MediaFileDto|false $file,
|
public bool|SplFileInfo $file,
|
||||||
public string $season,
|
public string $season,
|
||||||
public string $episode,
|
public string $episode,
|
||||||
public array $results
|
public array $results
|
||||||
|
|||||||
@@ -9,15 +9,12 @@ use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
|||||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
|
||||||
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\HttpKernel\Attribute\Cache;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Contracts\Cache\CacheInterface;
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
use Symfony\Contracts\Cache\ItemInterface;
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
use Symfony\UX\Turbo\TurboBundle;
|
|
||||||
|
|
||||||
final class WebController extends AbstractController
|
final class WebController extends AbstractController
|
||||||
{
|
{
|
||||||
@@ -27,9 +24,8 @@ final class WebController extends AbstractController
|
|||||||
private readonly Broadcaster $broadcaster,
|
private readonly Broadcaster $broadcaster,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Cache(expires: 3600, public: false, mustRevalidate: true)]
|
|
||||||
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
|
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
|
||||||
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache, Request $request): Response
|
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
|
||||||
{
|
{
|
||||||
$cacheId = sprintf(
|
$cacheId = sprintf(
|
||||||
"page.torrentio.movies.%s.%s",
|
"page.torrentio.movies.%s.%s",
|
||||||
@@ -37,22 +33,17 @@ final class WebController extends AbstractController
|
|||||||
$input->imdbId
|
$input->imdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input, $request) {
|
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||||
return $this->getMovieOptionsHandler->handle($input->toCommand());
|
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||||
|
return $this->render('torrentio/movies.html.twig', [
|
||||||
|
'results' => $results,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->headers->get('Turbo-Frame')) {
|
|
||||||
return $this->sendFragmentResponse($results, $request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('torrentio/movies.html.twig', [
|
|
||||||
'results' => $results,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
|
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
|
||||||
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
|
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
|
||||||
{
|
{
|
||||||
$cacheId = sprintf(
|
$cacheId = sprintf(
|
||||||
"page.torrentio.tvshows.%s.%s.%s.%s",
|
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||||
@@ -63,18 +54,13 @@ final class WebController extends AbstractController
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$results = $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||||
return $this->getTvShowOptionsHandler->handle($input->toCommand());
|
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||||
});
|
return $this->render('torrentio/tvshows.html.twig', [
|
||||||
|
'results' => $results,
|
||||||
if ($request->headers->get('Turbo-Frame')) {
|
]);
|
||||||
return $this->sendFragmentResponse($results, $request);
|
// });
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('torrentio/tvshows.html.twig', [
|
|
||||||
'results' => $results,
|
|
||||||
]);
|
|
||||||
} catch (TorrentioRateLimitException $exception) {
|
} catch (TorrentioRateLimitException $exception) {
|
||||||
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning');
|
$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',
|
return $this->render('bare.html.twig',
|
||||||
@@ -87,16 +73,29 @@ final class WebController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendFragmentResponse(ResultInterface $result, Request $request): Response
|
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
|
||||||
|
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
|
||||||
{
|
{
|
||||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
$cacheId = sprintf(
|
||||||
return $this->renderBlock(
|
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||||
'torrentio/fragments.html.twig',
|
$input->tmdbId,
|
||||||
$request->query->get('block'),
|
$input->imdbId,
|
||||||
[
|
$input->season,
|
||||||
'results' => $result,
|
$input->episode,
|
||||||
'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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class UtilExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[AsTwigFilter('episode_id_from_results')]
|
#[AsTwigFilter('episode_id_from_results')]
|
||||||
public function episodeIdFromResults($result): ?string
|
public function episodeId($result): ?string
|
||||||
{
|
{
|
||||||
if (!$result instanceof GetTvShowOptionsResult) {
|
if (!$result instanceof GetTvShowOptionsResult) {
|
||||||
return null;
|
return null;
|
||||||
@@ -56,11 +56,4 @@ class UtilExtension
|
|||||||
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
|
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
|
||||||
"E". str_pad($result->episode, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class LoginController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_getting_started');
|
return $this->redirectToRoute('app_getting_started');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config->authIs('oidc') && $config->bypassFormLogin()) {
|
if ($config->authIs('oidc') && true === $config->getAuthConfig()->getOidcConfig()->getBypassFormLogin()) {
|
||||||
return $this->redirectToRoute('app_login_oidc');
|
return $this->redirectToRoute('app_login_oidc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\User\Framework\Controller\Web;
|
namespace App\User\Framework\Controller\Web;
|
||||||
|
|
||||||
use App\Base\ConfigResolver;
|
use App\Base\ConfigResolver;
|
||||||
|
use App\Base\Service\Broadcaster;
|
||||||
use Drenso\OidcBundle\OidcClientInterface;
|
use Drenso\OidcBundle\OidcClientInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
@@ -15,13 +16,15 @@ class LoginOidcController extends AbstractController
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ConfigResolver $configResolver,
|
private ConfigResolver $configResolver,
|
||||||
|
private Broadcaster $broadcaster,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/login/oidc', name: 'app_login_oidc')]
|
#[Route('/login/oidc', name: 'app_login_oidc')]
|
||||||
public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse
|
public function oidcStart(OidcClientInterface $oidcClient): RedirectResponse
|
||||||
{
|
{
|
||||||
if (false === $this->configResolver->authIs('oidc')) {
|
if (false === $this->configResolver->authIs('oidc')) {
|
||||||
throw new \Exception('You must configure the OIDC environment variables before logging in at this route.');
|
$this->broadcaster->systemAlert('Your authentication must be set to "oidc" in order to login with OIDC.', 'warning');
|
||||||
|
return $this->redirectToRoute('app_login');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to authorization @ OIDC provider
|
// Redirect to authorization @ OIDC provider
|
||||||
|
|||||||
@@ -4,13 +4,9 @@ namespace App\User\Framework\Security;
|
|||||||
|
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use Drenso\OidcBundle\Exception\OidcException;
|
|
||||||
use Drenso\OidcBundle\Model\OidcTokens;
|
use Drenso\OidcBundle\Model\OidcTokens;
|
||||||
use Drenso\OidcBundle\Model\OidcUserData;
|
use Drenso\OidcBundle\Model\OidcUserData;
|
||||||
use Drenso\OidcBundle\Security\UserProvider\OidcUserProviderInterface;
|
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\OidcUser;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
@@ -47,7 +43,7 @@ class OidcUserProvider implements OidcUserProviderInterface
|
|||||||
|
|
||||||
public function supportsClass(string $class): bool
|
public function supportsClass(string $class): bool
|
||||||
{
|
{
|
||||||
return User::class === $class || OidcUser::class === $class;
|
return User::class === $class || OidcUser::class === $class || is_subclass_of($class, User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||||
|
|||||||
12
symfony.lock
12
symfony.lock
@@ -217,6 +217,18 @@
|
|||||||
"config/packages/messenger.yaml"
|
"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": {
|
"symfony/property-info": {
|
||||||
"version": "7.3",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -23,5 +23,17 @@
|
|||||||
<span class="text-sm">v{{ version }}</span>
|
<span class="text-sm">v{{ version }}</span>
|
||||||
</div>
|
</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('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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if download.mediaType == "tvshows" and download.episodeId != null %}
|
{% if download.mediaType == "tvshows" and download.episodeId != null %}
|
||||||
— <span class="ml-1">({{ download.episodeId }})</span>
|
— <span class="ml-1">(S{{ download.episodeId }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -44,12 +44,7 @@
|
|||||||
|
|
||||||
{% set delete_button = component('ux:icon', {name: 'ic:twotone-cancel', height: '17.75px', width: '17.75px', class: 'rounded-full align-middle text-red-600 hover:text-red-700' }) %}
|
{% set delete_button = component('ux:icon', {name: 'ic:twotone-cancel', height: '17.75px', width: '17.75px', class: 'rounded-full align-middle text-red-600 hover:text-red-700' }) %}
|
||||||
<twig:Modal heading="But wait!" button_text="{{ delete_button }}" submit_action="{{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}" show_cancel show_submit>
|
<twig:Modal heading="But wait!" button_text="{{ delete_button }}" submit_action="{{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}" show_cancel show_submit>
|
||||||
<p class="mb-1">Are you sure you want to delete the following record?</p>
|
Are you sure you want to delete <span class="font-bold">{{ download.filename }}</span>?
|
||||||
<p class="mb-1 ml-4 italic">{{ download.filename }}</p>
|
|
||||||
<div class="">
|
|
||||||
<input id="delete_file_{{ download.id }}" class="accent-orange-500" type="checkbox" value="false" name="delete_file" />
|
|
||||||
<label for="delete_file_{{ download.id }}">Delete the file as well?</label>
|
|
||||||
</div>
|
|
||||||
</twig:Modal>
|
</twig:Modal>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -26,6 +26,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="fixed z-40 top-10 right-10">
|
||||||
<div class="z-40">
|
<div class="z-40">
|
||||||
<ul id="alert_list" class="flex flex-col gap-2">
|
<ul id="alert_list" class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
mediaType: mediaType,
|
mediaType: mediaType,
|
||||||
imdbId: imdbId
|
imdbId: imdbId
|
||||||
}) }}">
|
}) }}">
|
||||||
<img src="{{ preload(image) }}" class="w-full md:w-40 rounded-md" />
|
<img src="{{ image }}" class="w-full md:w-40 rounded-md" />
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ path('app_search_result', {
|
<a href="{{ path('app_search_result', {
|
||||||
mediaType: mediaType,
|
mediaType: mediaType,
|
||||||
imdbId: imdbId
|
imdbId: imdbId
|
||||||
}) }}">
|
}) }}">
|
||||||
<h3 class="text-center text-white md:text-md md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
<h3 class="text-center text-white md:text-xl md:text-base md:max-w-[16ch]">{{ title }}</h3>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,17 +40,30 @@
|
|||||||
{{ episode['air_date']|date(null, 'UTC') }}
|
{{ episode['air_date']|date(null, 'UTC') }}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
{% if episode['file'] != false %}
|
||||||
title: this.title,
|
<span data-controller="popover">
|
||||||
season: episode['season_number'],
|
<template data-popover-target="content">
|
||||||
episode: episode['episode_number'],
|
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||||
block: 'media_exists_badge',
|
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||||
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
<ul class="list-disc ml-3">
|
||||||
}) }}">
|
<li class="font-normal">{{ episode['file'].realPath|strip_media_path }} — <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 %}
|
||||||
<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.">
|
<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
|
missing
|
||||||
</small>
|
</small>
|
||||||
</twig:Turbo:Frame>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 justify-between">
|
<div class="flex flex-col gap-4 justify-between">
|
||||||
@@ -74,15 +87,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-block overflow-hidden rounded-lg">
|
<div {{ stimulus_target('tv-results', 'listContainer') }} 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{% 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 }} — <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 %}
|
|
||||||
@@ -49,32 +49,9 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-gray-50">
|
<p class="text-gray-50">
|
||||||
{{ results.media.description }}
|
{{ results.media.description }}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,12 +62,6 @@
|
|||||||
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
||||||
data-movie-results-loading-icon-outlet=".loading-icon"
|
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>
|
</div>
|
||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{% 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>
|
|
||||||
<div id="{{ target }}">
|
|
||||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'tv-results'}) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</turbo-stream>
|
|
||||||
{% endblock %}
|
|
||||||
20
templates/torrentio/index.html.twig
Normal file
20
templates/torrentio/index.html.twig
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
<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="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 }} — <strong>{{ results.file.size|filesize }}</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="overflow-hidden rounded-md">
|
<div class="overflow-hidden rounded-md">
|
||||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user