Compare commits

...

14 Commits

30 changed files with 703 additions and 91 deletions

View File

@@ -0,0 +1,30 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
initialize() {}
connect() {}
disconnect() {}
async clearAll() {
let response = await fetch('/api/torrentio/cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
type: 'torrentio',
mediaType: 'tvshows',
})
});
response = await response.json()
}
}

View File

@@ -12,6 +12,7 @@ export default class extends Controller {
filename: String, filename: String,
mediaType: String, mediaType: String,
imdbId: String, imdbId: String,
episodeId: String
} }
download() { download() {
@@ -27,6 +28,7 @@ export default class extends Controller {
filename: this.filenameValue, filename: this.filenameValue,
mediaType: this.mediaTypeValue, mediaType: this.mediaTypeValue,
imdbId: this.imdbIdValue, imdbId: this.imdbIdValue,
episodeId: this.episodeIdValue
}) })
}) })
.then(res => res.json()) .then(res => res.json())

View File

@@ -25,6 +25,13 @@
.alert-warning { .alert-warning {
@apply bg-yellow-500/70 hover:bg-yellow-600 border-yellow-400 text-black @apply bg-yellow-500/70 hover:bg-yellow-600 border-yellow-400 text-black
} }
.primary-btn {
@apply px-1 py-1 rounded-md bg-orange-500 self-end text-white w-16 ml-2 hover:bg-orange-600
}
.secondary-btn {
@apply px-1 py-1 rounded-md self-end w-16 hover:bg-stone-100
}
} }
/* Prevent scrolling while dialog is open */ /* Prevent scrolling while dialog is open */

View File

@@ -22,6 +22,7 @@
"nihilarr/parse-torrent-name": "^0.0.1", "nihilarr/parse-torrent-name": "^0.0.1",
"nyholm/psr7": "*", "nyholm/psr7": "*",
"p3k/emoji-detector": "^1.2", "p3k/emoji-detector": "^1.2",
"php-http/cache-plugin": "^2.0",
"php-tmdb/api": "^4.1", "php-tmdb/api": "^4.1",
"predis/predis": "^2.4", "predis/predis": "^2.4",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",

370
composer.lock generated
View File

@@ -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": "3b0840f4e60d44d341c934f6ca153944", "content-hash": "67e697578f7237f60726c0d93bfed001",
"packages": [ "packages": [
{ {
"name": "1tomany/rich-bundle", "name": "1tomany/rich-bundle",
@@ -285,6 +285,72 @@
}, },
"time": "2021-10-17T22:52:23+00:00" "time": "2021-10-17T22:52:23+00:00"
}, },
{
"name": "clue/stream-filter",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/clue/stream-filter.git",
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"Clue\\StreamFilter\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "A simple and modern approach to stream filtering in PHP",
"homepage": "https://github.com/clue/stream-filter",
"keywords": [
"bucket brigade",
"callback",
"filter",
"php_user_filter",
"stream",
"stream_filter_append",
"stream_filter_register"
],
"support": {
"issues": "https://github.com/clue/stream-filter/issues",
"source": "https://github.com/clue/stream-filter/tree/v1.7.0"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2023-12-20T15:40:13+00:00"
},
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "3.4.3", "version": "3.4.3",
@@ -2865,6 +2931,130 @@
}, },
"time": "2024-02-19T18:29:05+00:00" "time": "2024-02-19T18:29:05+00:00"
}, },
{
"name": "php-http/cache-plugin",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-http/cache-plugin.git",
"reference": "5c591e9e04602cec12307e3e1be3abefeb005e29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/cache-plugin/zipball/5c591e9e04602cec12307e3e1be3abefeb005e29",
"reference": "5c591e9e04602cec12307e3e1be3abefeb005e29",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
"php-http/client-common": "^1.9 || ^2.0",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"psr/http-factory-implementation": "^1.0",
"symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"require-dev": {
"nyholm/psr7": "^1.6.1",
"phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Client\\Common\\Plugin\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "PSR-6 Cache plugin for HTTPlug",
"homepage": "http://httplug.io",
"keywords": [
"cache",
"http",
"httplug",
"plugin"
],
"support": {
"issues": "https://github.com/php-http/cache-plugin/issues",
"source": "https://github.com/php-http/cache-plugin/tree/2.0.1"
},
"time": "2024-10-02T11:25:38+00:00"
},
{
"name": "php-http/client-common",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/php-http/client-common.git",
"reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46",
"reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
"php-http/httplug": "^2.0",
"php-http/message": "^1.6",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0 || ^2.0",
"symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0",
"symfony/polyfill-php80": "^1.17"
},
"require-dev": {
"doctrine/instantiator": "^1.1",
"guzzlehttp/psr7": "^1.4",
"nyholm/psr7": "^1.2",
"phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
"phpspec/prophecy": "^1.10.2",
"phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7"
},
"suggest": {
"ext-json": "To detect JSON responses with the ContentTypePlugin",
"ext-libxml": "To detect XML responses with the ContentTypePlugin",
"php-http/cache-plugin": "PSR-6 Cache plugin",
"php-http/logger-plugin": "PSR-3 Logger plugin",
"php-http/stopwatch-plugin": "Symfony Stopwatch plugin"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Client\\Common\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "Common HTTP Client implementations and tools for HTTPlug",
"homepage": "http://httplug.io",
"keywords": [
"client",
"common",
"http",
"httplug"
],
"support": {
"issues": "https://github.com/php-http/client-common/issues",
"source": "https://github.com/php-http/client-common/tree/2.7.2"
},
"time": "2024-09-24T06:21:48+00:00"
},
{ {
"name": "php-http/discovery", "name": "php-http/discovery",
"version": "1.20.0", "version": "1.20.0",
@@ -2944,6 +3134,184 @@
}, },
"time": "2024-10-02T11:20:13+00:00" "time": "2024-10-02T11:20:13+00:00"
}, },
{
"name": "php-http/httplug",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/php-http/httplug.git",
"reference": "5cad731844891a4c282f3f3e1b582c46839d22f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4",
"reference": "5cad731844891a4c282f3f3e1b582c46839d22f4",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
"php-http/promise": "^1.1",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0 || ^2.0"
},
"require-dev": {
"friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
"phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eric GELOEN",
"email": "geloen.eric@gmail.com"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "HTTPlug, the HTTP client abstraction for PHP",
"homepage": "http://httplug.io",
"keywords": [
"client",
"http"
],
"support": {
"issues": "https://github.com/php-http/httplug/issues",
"source": "https://github.com/php-http/httplug/tree/2.4.1"
},
"time": "2024-09-23T11:39:58+00:00"
},
{
"name": "php-http/message",
"version": "1.16.2",
"source": {
"type": "git",
"url": "https://github.com/php-http/message.git",
"reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
"reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
"shasum": ""
},
"require": {
"clue/stream-filter": "^1.5",
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.1 || ^2.0"
},
"provide": {
"php-http/message-factory-implementation": "1.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.6",
"ext-zlib": "*",
"guzzlehttp/psr7": "^1.0 || ^2.0",
"laminas/laminas-diactoros": "^2.0 || ^3.0",
"php-http/message-factory": "^1.0.2",
"phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
"slim/slim": "^3.0"
},
"suggest": {
"ext-zlib": "Used with compressor/decompressor streams",
"guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
"laminas/laminas-diactoros": "Used with Diactoros Factories",
"slim/slim": "Used with Slim Framework PSR-7 implementation"
},
"type": "library",
"autoload": {
"files": [
"src/filters.php"
],
"psr-4": {
"Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "HTTP Message related tools",
"homepage": "http://php-http.org",
"keywords": [
"http",
"message",
"psr-7"
],
"support": {
"issues": "https://github.com/php-http/message/issues",
"source": "https://github.com/php-http/message/tree/1.16.2"
},
"time": "2024-10-02T11:34:13+00:00"
},
{
"name": "php-http/promise",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/php-http/promise.git",
"reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
"reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3",
"phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joel Wurtz",
"email": "joel.wurtz@gmail.com"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "Promise used for asynchronous HTTP requests",
"homepage": "http://httplug.io",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/php-http/promise/issues",
"source": "https://github.com/php-http/promise/tree/1.3.1"
},
"time": "2024-03-15T13:55:21+00:00"
},
{ {
"name": "php-tmdb/api", "name": "php-tmdb/api",
"version": "4.1.3", "version": "4.1.3",

View File

@@ -15,5 +15,11 @@ framework:
#app: cache.adapter.apcu #app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default # Namespaced pools use the above "app" backend by default
#pools: pools:
#my.dedicated.cache: null torrentio.cache:
adapter: cache.app
tmdb.cache:
adapter: cache.app
default_lifetime: 2592000
page.cache:
adapter: cache.app

View File

@@ -29,3 +29,12 @@ controllersMonitor:
type: attribute type: attribute
defaults: defaults:
schemes: ['https'] schemes: ['https']
controllersTorrentio:
resource:
path: ../src/Torrentio/Framework/Controller
namespace: App\Torrentio\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]

View File

@@ -27,7 +27,7 @@ DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-Ma
# Popular Movies and TV Shows section. # Popular Movies and TV Shows section.
#TMDB_API= #TMDB_API=
REAL_DEBRID_KEY="QYYBR7OSQ4VEFKWASDEZ2B4VO67KHUJY6IWOT7HHA7ATXO7QCYDQ" REAL_DEBRID_KEY=""
TMDB_API=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0ZTJjYjJhOGUzOGJhNjdiNjVhOGU1NGM0ZWI1MzhmOCIsIm5iZiI6MTczNzkyNjA0NC41NjQsInN1YiI6IjY3OTZhNTljYzdiMDFiNzJjNzIzZWM5YiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.e8DbNe9qrSBC1y-ANRv-VWBAtls-ZS2r7aNCiI68mpw TMDB_API=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0ZTJjYjJhOGUzOGJhNjdiNjVhOGU1NGM0ZWI1MzhmOCIsIm5iZiI6MTczNzkyNjA0NC41NjQsInN1YiI6IjY3OTZhNTljYzdiMDFiNzJjNzIzZWM5YiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.e8DbNe9qrSBC1y-ANRv-VWBAtls-ZS2r7aNCiI68mpw

View File

@@ -11,6 +11,13 @@ services:
- '8006:80' - '8006:80'
env_file: env_file:
- .env - .env
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on: depends_on:
database: database:
condition: service_healthy condition: service_healthy
@@ -27,6 +34,8 @@ services:
volumes: volumes:
- /mnt/media/downloads/movies:/var/download/movies - /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows - /mnt/media/downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
command: -vvv command: -vvv
env_file: env_file:
- .env - .env
@@ -43,35 +52,17 @@ services:
scheduler: scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest image: code.caldwell.digital/home/torsearch-scheduler:latest
volumes: volumes:
- ./downloads:/var/download - /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
env_file: env_file:
- .env - .env
environment:
TZ: America/Chicago
restart: always restart: always
depends_on: depends_on:
app: app:
condition: service_healthy condition: service_healthy
# This container facilitates viewing the progress of downloads
# in realtime. It also handles sending alerts and notifications.
# The MERCURE_PUBLISHER_JWT key & MERCURE_SUBSCRIBER_JWT_KEY should
# match the MERCURE_JWT_SECRET environment variable.
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3001:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database: database:
image: mariadb:10.11.2 image: mariadb:10.11.2
volumes: volumes:

View File

@@ -40,8 +40,6 @@ final class SearchController extends AbstractController
): Response { ): Response {
$result = $this->getMediaInfoHandler->handle($input->toCommand()); $result = $this->getMediaInfoHandler->handle($input->toCommand());
// $this->warmDownloadOptionCache($result->media);
return $this->render('search/result.html.twig', [ return $this->render('search/result.html.twig', [
'results' => $result, 'results' => $result,
'filter' => [ 'filter' => [

View File

@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Response;
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\Contracts\Cache\TagAwareCacheInterface;
final class TorrentioController extends AbstractController final class TorrentioController extends AbstractController
{ {
@@ -25,7 +26,7 @@ final class TorrentioController extends AbstractController
) {} ) {}
#[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): Response public function movieOptions(GetMovieOptionsInput $input, TagAwareCacheInterface $pageCache): Response
{ {
$cacheId = sprintf( $cacheId = sprintf(
"page.torrentio.movies.%s.%s", "page.torrentio.movies.%s.%s",
@@ -33,17 +34,29 @@ final class TorrentioController extends AbstractController
$input->imdbId $input->imdbId
); );
return $cache->get($cacheId, function (ItemInterface $item) use ($input) { try {
return $pageCache->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));
$item->tag(['page', 'page.torrentio', 'page.torrentio.movies', "page.torrentio.movies.$input->tmdbId.$input->imdbId", 'torrentio', 'torrentio.movies', "torrentio.movies.$input->tmdbId.$input->imdbId"]);
$results = $this->getMovieOptionsHandler->handle($input->toCommand()); $results = $this->getMovieOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/movies.html.twig', [ return $this->render('torrentio/movies.html.twig', [
'results' => $results, 'results' => $results,
]); ]);
}); });
} catch (TorrentioRateLimitException $exception) {
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning');
return $this->render('bare.html.twig',
[],
new Response('Too many requests',
Response::HTTP_TOO_MANY_REQUESTS,
['Retry-After' => 4000]
)
);
}
} }
#[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): Response public function tvShowOptions(GetTvShowOptionsInput $input, TagAwareCacheInterface $pageCache): Response
{ {
$cacheId = sprintf( $cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s", "page.torrentio.tvshows.%s.%s.%s.%s",
@@ -54,8 +67,9 @@ final class TorrentioController extends AbstractController
); );
try { try {
return $cache->get($cacheId, function (ItemInterface $item) use ($input) { return $pageCache->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));
$item->tag(['page', 'page.torrentio', 'page.torrentio.tvshows', "page.torrentio.tvshows.$input->tmdbId.$input->imdbId", "page.torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season", "page.torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season.$input->episode", 'torrentio', 'torrentio.tvshows', "torrentio.tvshows.$input->tmdbId.$input->imdbId", "torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season", "torrentio.tvshows.$input->tmdbId.$input->imdbId.$input->season.$input->episode", $input->imdbId, $input->tmdbId]);
$results = $this->getTvShowOptionsHandler->handle($input->toCommand()); $results = $this->getTvShowOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/tvshows.html.twig', [ return $this->render('torrentio/tvshows.html.twig', [
'results' => $results, 'results' => $results,

View File

@@ -26,6 +26,9 @@ class DownloadMediaInput implements InputInterface
#[SourceRequest('imdbId')] #[SourceRequest('imdbId')]
public string $imdbId, public string $imdbId,
#[SourceRequest('episodeId', nullify: true)]
public ?string $episodeId = null,
public ?int $userId = null, public ?int $userId = null,
public ?int $downloadId = null, public ?int $downloadId = null,

View File

@@ -11,6 +11,7 @@ use App\Download\Action\Input\PauseDownloadInput;
use App\Download\Action\Input\ResumeDownloadInput; use App\Download\Action\Input\ResumeDownloadInput;
use App\Download\Framework\Repository\DownloadRepository; use App\Download\Framework\Repository\DownloadRepository;
use App\Util\Broadcaster; use App\Util\Broadcaster;
use Nihilarr\PTN;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
@@ -28,6 +29,13 @@ class ApiController extends AbstractController
public function download( public function download(
DownloadMediaInput $input, DownloadMediaInput $input,
): Response { ): Response {
$ptn = (object) new Ptn()->parse($input->filename);
if ($input->mediaType === "tvshows" &&
!property_exists($ptn, 'episode') && !property_exists($ptn, 'season')
) {
$input->filename = $input->episodeId . '_' . $input->filename;
}
$download = $this->downloadRepository->insert( $download = $this->downloadRepository->insert(
$this->getUser(), $this->getUser(),
$input->url, $input->url,

View File

@@ -2,17 +2,13 @@
namespace App\Monitor\Framework\Controller; namespace App\Monitor\Framework\Controller;
use App\Download\Action\Input\DeleteDownloadInput;
use App\Monitor\Action\Handler\AddMonitorHandler; use App\Monitor\Action\Handler\AddMonitorHandler;
use App\Monitor\Action\Handler\DeleteMonitorHandler; use App\Monitor\Action\Handler\DeleteMonitorHandler;
use App\Monitor\Action\Input\AddMonitorInput; use App\Monitor\Action\Input\AddMonitorInput;
use App\Monitor\Action\Input\DeleteMonitorInput; use App\Monitor\Action\Input\DeleteMonitorInput;
use App\Util\Broadcaster; use App\Util\Broadcaster;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
class ApiController extends AbstractController class ApiController extends AbstractController
@@ -46,19 +42,9 @@ class ApiController extends AbstractController
public function deleteMonitor( public function deleteMonitor(
DeleteMonitorInput $input, DeleteMonitorInput $input,
DeleteMonitorHandler $handler, DeleteMonitorHandler $handler,
HubInterface $hub,
) { ) {
$response = $handler->handle($input->toCommand()); $response = $handler->handle($input->toCommand());
$hub->publish(new Update(
'alerts',
$this->renderer->render('broadcast/Alert.stream.html.twig', [
'alert_id' => uniqid(),
'title' => 'Success',
'message' => "New monitor added for {$response->monitor->getTitle()}",
])
));
return $this->json([ return $this->json([
'status' => 200, 'status' => 200,
'message' => $response 'message' => $response

View File

@@ -6,6 +6,8 @@ use Aimeos\Map;
use App\Enum\MediaType; use App\Enum\MediaType;
use App\ValueObject\ResultFactory; use App\ValueObject\ResultFactory;
use Carbon\Carbon; use Carbon\Carbon;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\RedisAdapter;
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\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
@@ -13,6 +15,7 @@ use Symfony\Contracts\Cache\ItemInterface;
use Tmdb\Api\Find; use Tmdb\Api\Find;
use Tmdb\Client; use Tmdb\Client;
use Tmdb\Event\BeforeRequestEvent; use Tmdb\Event\BeforeRequestEvent;
use Tmdb\Event\Listener\Psr6CachedRequestListener;
use Tmdb\Event\Listener\Request\AcceptJsonRequestListener; use Tmdb\Event\Listener\Request\AcceptJsonRequestListener;
use Tmdb\Event\Listener\Request\ApiTokenRequestListener; use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener; use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
@@ -41,7 +44,7 @@ class Tmdb
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500"; const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
public function __construct( public function __construct(
private readonly CacheInterface $cache, private readonly CacheItemPoolInterface $tmdbCache,
private readonly EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
#[Autowire(env: 'TMDB_API')] string $apiKey, #[Autowire(env: 'TMDB_API')] string $apiKey,
) { ) {
@@ -72,7 +75,13 @@ class Tmdb
/** /**
* Required event listeners and events to be registered with the PSR-14 Event Dispatcher. * Required event listeners and events to be registered with the PSR-14 Event Dispatcher.
*/ */
$requestListener = new RequestListener($this->client->getHttpClient(), $this->eventDispatcher); $requestListener = new Psr6CachedRequestListener(
$this->client->getHttpClient(),
$this->eventDispatcher,
$tmdbCache,
$this->client->getHttpClient()->getPsr17StreamFactory(),
[]
);
$this->eventDispatcher->addListener(RequestEvent::class, $requestListener); $this->eventDispatcher->addListener(RequestEvent::class, $requestListener);
$apiTokenListener = new ApiTokenRequestListener($this->client->getToken()); $apiTokenListener = new ApiTokenRequestListener($this->client->getToken());
@@ -316,7 +325,7 @@ class Tmdb
public function getImdbId(string $tmdbId, $mediaType) public function getImdbId(string $tmdbId, $mediaType)
{ {
$externalIds = $this->cache->get("tmdb.externalIds.{$tmdbId}", $externalIds = $this->tmdbCache->get("tmdb.externalIds.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) { function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) { switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value: case MediaType::Movie->value:
@@ -337,7 +346,7 @@ class Tmdb
public function getImages($tmdbId, $mediaType) public function getImages($tmdbId, $mediaType)
{ {
return $this->cache->get("tmdb.images.{$tmdbId}", return $this->tmdbCache->get("tmdb.images.{$tmdbId}",
function (ItemInterface $item) use ($tmdbId, $mediaType) { function (ItemInterface $item) use ($tmdbId, $mediaType) {
switch (MediaType::tryFrom($mediaType)->value) { switch (MediaType::tryFrom($mediaType)->value) {
case MediaType::Movie->value: case MediaType::Movie->value:

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Torrentio\Action\Command;
use OneToMany\RichBundle\Contract\CommandInterface;
class DeleteCacheCommand implements CommandInterface
{
public function __construct(
public ?string $type = null,
public ?string $mediaType = null,
public ?string $tmdbId = null,
public ?string $imdbId = null,
public ?int $season = null,
public ?int $episode = null,
public ?array $tags = null,
) {}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Torrentio\Action\Handler;
use Aimeos\Map;
use App\Torrentio\Action\Command\DeleteCacheCommand;
use App\Torrentio\Action\Result\DeleteCacheResult;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\HandlerInterface;
use OneToMany\RichBundle\Contract\ResultInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/** @implements HandlerInterface<DeleteCacheCommand, DeleteCacheResult> */
class DeleteCacheHandler implements HandlerInterface
{
public function __construct(
private readonly TagAwareCacheInterface $torrentioCache
) {}
public function handle(CommandInterface $command): ResultInterface
{
$input = Map::from((array) $command)
->filter(fn ($value, $key) => null !== $value && "" !== $value)
;
$cacheKey = null;
if ($input->has('type')) {
$cacheKey = $input->get('type');
if ($input->has('mediaType')) {
$cacheKey .= ".".$input->get('mediaType');
if ($input->has('tmdbId')) {
$cacheKey .= ".".$input->get('tmdbId');
if ($input->has('imdbId')) {
$cacheKey .= ".".$input->get('imdbId');
if ($input->has('season')) {
$cacheKey .= ".".$input->get('season');
if ($input->has('episode')) {
$cacheKey .= ".".$input->get('episode');
}
}
}
}
}
}
if ($cacheKey !== null) {
$this->torrentioCache->invalidateTags([$cacheKey]);
}
if ($input->has('tags')) {
$this->torrentioCache->invalidateTags($input->get('tags'));
}
return new DeleteCacheResult($input, $cacheKey, $command->tags);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Torrentio\Action\Input;
use App\Torrentio\Action\Command\DeleteCacheCommand;
use OneToMany\RichBundle\Attribute\SourceRequest;
use OneToMany\RichBundle\Contract\CommandInterface;
use OneToMany\RichBundle\Contract\InputInterface;
/**
* @implements DeleteCacheInput<DeleteCacheCommand>
*/
class DeleteCacheInput implements InputInterface
{
public function __construct(
#[SourceRequest('type', nullify: true)]
public ?string $type,
#[SourceRequest('mediaType', nullify: true)]
public ?string $mediaType,
#[SourceRequest('tmdbId', nullify: true)]
public ?string $tmdbId,
#[SourceRequest('imdbId', nullify: true)]
public ?string $imdbId,
#[SourceRequest('season', nullify: true)]
public ?int $season,
#[SourceRequest('episode', nullify: true)]
public ?int $episode,
#[SourceRequest('tags', nullify: true)]
public ?array $tags,
) {}
public function toCommand(): CommandInterface
{
return new DeleteCacheCommand(
type: $this->type,
mediaType: $this->mediaType,
tmdbId: $this->tmdbId,
imdbId: $this->imdbId,
season: $this->season,
episode: $this->episode
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Torrentio\Action\Result;
use Aimeos\Map;
use OneToMany\RichBundle\Contract\ResultInterface;
class DeleteCacheResult implements ResultInterface
{
public function __construct(
public Map $result,
public ?string $cacheKey = null,
public ?array $tags = null,
) {}
}

View File

@@ -10,8 +10,8 @@ use App\Torrentio\Exception\TorrentioRateLimitException;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class Torrentio class Torrentio
{ {
@@ -23,7 +23,7 @@ class Torrentio
public function __construct( public function __construct(
#[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey, #[Autowire(env: 'REAL_DEBRID_KEY')] private string $realDebridKey,
private CacheInterface $cache, private TagAwareCacheInterface $torrentioCache,
private LoggerInterface $logger, private LoggerInterface $logger,
) { ) {
$this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl); $this->searchUrl = str_replace('{realDebridKey}', $this->realDebridKey, $this->baseUrl);
@@ -36,8 +36,9 @@ class Torrentio
{ {
$cacheKey = "torrentio.{$imdbCode}"; $cacheKey = "torrentio.{$imdbCode}";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode) { $results = $this->torrentioCache->get($cacheKey, function (ItemInterface $item) use ($imdbCode, $type) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['torrentio', $type, $imdbCode]);
try { try {
$response = $this->client->get("$this->searchUrl/$imdbCode.json"); $response = $this->client->get("$this->searchUrl/$imdbCode.json");
return json_decode( return json_decode(
@@ -61,8 +62,9 @@ class Torrentio
public function fetchEpisodeResults(string $imdbId, int $season, int $episode): array public function fetchEpisodeResults(string $imdbId, int $season, int $episode): array
{ {
$cacheKey = "torrentio.$imdbId.$season.$episode"; $cacheKey = "torrentio.$imdbId.$season.$episode";
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) { $results = $this->torrentioCache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0)); $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$item->tag(['torrentio', 'tvshows', 'torrentio.tvshows', $imdbId, "torrentio.$imdbId", "$imdbId.$season", "torrentio.$imdbId.$season", "$imdbId.$season.$episode", "torrentio.$imdbId.$season.$episode"]);
try { try {
$response = $this->client->get("$this->searchUrl/$imdbId:$season:$episode.json"); $response = $this->client->get("$this->searchUrl/$imdbId:$season:$episode.json");
return json_decode( return json_decode(

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Torrentio\Framework\Controller;
use App\Torrentio\Action\Handler\DeleteCacheHandler;
use App\Torrentio\Action\Input\DeleteCacheInput;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ApiController extends AbstractController
{
#[Route('/api/torrentio/cache', name: 'api.torrentio.cache', methods: ['POST'])]
public function deleteCache(
DeleteCacheInput $deleteCacheInput,
DeleteCacheHandler $deleteCacheHandler,
): Response {
$result = $deleteCacheHandler->handle($deleteCacheInput->toCommand());
return $this->json($result, Response::HTTP_OK);
}
}

View File

@@ -4,7 +4,10 @@ namespace App\Twig\Extensions;
use App\Monitor\Framework\Entity\Monitor; use App\Monitor\Framework\Entity\Monitor;
use App\Monitor\Service\MediaFiles; use App\Monitor\Service\MediaFiles;
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
use App\Torrentio\Result\TorrentioResult;
use ChrisUllyott\FileSize; use ChrisUllyott\FileSize;
use Tmdb\Model\Tv\Episode;
use Twig\Attribute\AsTwigFilter; use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction; use Twig\Attribute\AsTwigFunction;
@@ -45,4 +48,15 @@ class UtilExtension
$path $path
); );
} }
#[AsTwigFilter('episode_id_from_results')]
public function episodeId($result): ?string
{
if (!$result instanceof GetTvShowOptionsResult) {
return null;
}
return "S". str_pad($result->season, 2, "0", STR_PAD_LEFT) .
"E". str_pad($result->episode, 2, "0", STR_PAD_LEFT);
}
} }

View File

@@ -17,13 +17,16 @@ use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
class RegistrationController extends AbstractController class RegistrationController extends AbstractController
{ {
public function __construct(private readonly RegisterUserHandler $registerUserHandler) public function __construct(private readonly RegisterUserHandler $registerUserHandler,
private readonly RequestStack $requestStack
)
{ {
} }
@@ -71,6 +74,7 @@ class RegistrationController extends AbstractController
)); ));
$security->login($user->user); $security->login($user->user);
$this->requestStack->getCurrentRequest()->getSession()->set('mercure_alert_topic', 'alerts_' . uniqid());
return $this->redirectToRoute('app_index'); return $this->redirectToRoute('app_index');
} }

View File

@@ -20,7 +20,7 @@
<div class="col-span-5 h-screen overflow-y-scroll"> <div class="col-span-5 h-screen overflow-y-scroll">
<twig:Header /> <twig:Header />
<div class="px-4 mt-3 flex flex-row justify-between"> <div class="px-4 mt-3 flex flex-row justify-between">
<h2 class="text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2> <h2 class="m-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
<div class="flex flex-row gap-1 align-end justify-end items-end"> <div class="flex flex-row gap-1 align-end justify-end items-end">
{% block action_buttons %}{% endblock %} {% block action_buttons %}{% endblock %}
</div> </div>

View File

@@ -9,12 +9,12 @@
{% if show_cancel is defined or show_submit is defined %} {% if show_cancel is defined or show_submit is defined %}
<div class="flex justify-end"> <div class="flex justify-end">
{% if show_cancel is defined %} {% if show_cancel is defined %}
<button type="button" data-action="dialog#close" class="px-1 py-1 rounded-md self-end w-16 hover:bg-stone-100" autofocus> <button type="button" data-action="dialog#close" class="secondary-btn" autofocus>
{{ cancel_text|default('Cancel') }} {{ cancel_text|default('Cancel') }}
</button> </button>
{% endif %} {% endif %}
{% if show_submit is defined %} {% if show_submit is defined %}
<button type="button" {{ submit_action|raw }} class="px-1 py-1 rounded-md bg-orange-500 self-end text-white w-16 ml-2 hover:bg-orange-600" autofocus> <button type="button" {{ submit_action|raw }} class="primary-btn" autofocus>
{{ submit_text|default('Submit') }} {{ submit_text|default('Submit') }}
</button> </button>
{% endif %} {% endif %}
@@ -22,5 +22,5 @@
{% endif %} {% endif %}
</dialog> </dialog>
<button type="button" data-action="dialog#open">{{ button_text|raw }}</button> <button type="button" data-action="dialog#open" class="{{ button_class|default('') }}">{{ button_text|raw }}</button>
</div> </div>

View File

@@ -9,7 +9,6 @@
<twig:DownloadList type="active" :isWidget="false" :perPage="10"></twig:DownloadList> <twig:DownloadList type="active" :isWidget="false" :perPage="10"></twig:DownloadList>
</twig:Card> </twig:Card>
</div> </div>
<div class="p-4"> <div class="p-4">
<twig:Card title="Recent Downloads"> <twig:Card title="Recent Downloads">
<twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList> <twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList>

View File

@@ -1,10 +1,10 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Dashboard &mdash; Torsearch{% endblock %} {% block title %}Dashboard &mdash; Torsearch{% endblock %}
{% block h2 %}Dashboard{% endblock %}
{% block body %} {% block body %}
<div class="p-4 flex flex-col grow gap-4 z-30"> <div class="p-4 flex flex-col grow gap-4 z-30">
<h2 class="mb-2 text-3xl font-bold text-gray-50">Dashboard</h2>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<twig:Card title="Active Downloads" class="w-full"> <twig:Card title="Active Downloads" class="w-full">
<twig:DownloadList :type="'active'" /> <twig:DownloadList :type="'active'" />

View File

@@ -4,22 +4,14 @@
{% block h2 %}Monitors{% endblock %} {% block h2 %}Monitors{% endblock %}
{% block body %} {% block body %}
<div class="flex flex-row"> <div class="p-4">
<twig:Card title="Active Monitors" class="w-full">
<div class="p-2 flex flex-col gap-4">
<twig:Card title="Active Monitors">
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList> <twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card> </twig:Card>
<twig:Card title="Complete Monitors"> </div>
<div class="p-4">
<twig:Card title="Complete Monitors" class="w-full">
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList> <twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
</twig:Card> </twig:Card>
</div> </div>
<div class="p-2">
<twig:Card title="Upcoming Episodes" >
<twig:UpcomingEpisodes />
</twig:Card>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -34,7 +34,7 @@
</thead> </thead>
<tbody> <tbody>
{% for result in results.results %} {% for result in results.results %}
<tr class="bg-white border-b dark:bg-slate-700 dark:border-gray-600 border-gray-200" data-provider="{{ result.provider }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }} {% endif %}"> <tr class="bg-white border-b dark:bg-slate-700 dark:border-gray-600 border-gray-200" data-provider="{{ result.provider }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
<td id="size" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50"> <td id="size" class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.size }} {{ result.size }}
</td> </td>
@@ -60,7 +60,8 @@
title: results.media.title, title: results.media.title,
filename: result.filename, filename: result.filename,
mediaType: results.media.mediaType, mediaType: results.media.mediaType,
imdbId: results.media.imdbId imdbId: results.media.imdbId,
episodeId: results|episode_id_from_results
}) }} }) }}
{{ stimulus_action('download_button', 'download', 'click') }} {{ stimulus_action('download_button', 'download', 'click') }}
> >

View File

@@ -3,11 +3,15 @@
{% block h2 %}Preferences{% endblock %} {% block h2 %}Preferences{% endblock %}
{% block action_buttons %} {% block action_buttons %}
<button <div {{ stimulus_controller('clear_cache') }}>
class="px-1.5 py-1 my-2 text-white text-sm bg-blue-950 hover:bg-black/80 border-2 border-blue-500/90 rounded-md inline-block" <twig:Modal heading="Hold on a sec!" button_text="Clear Cache" cancel_text="Nope" submit_text="Yep" show_cancel show_submit
button_class="px-1.5 py-1 my-2 text-white text-sm bg-blue-950 hover:bg-black/80 border-2 border-blue-500/90 rounded-md inline-block"
submit_action="{{ stimulus_action('clear_cache', 'clearAll', 'click') }}"
> >
Clear Cache This will clear the TMDB, Torrentio, and application cache. Clearing the cache is safe, but may lead to
</button> slower page loads and rate limits by Torrentio. Would you like to proceed?
</twig:Modal>
</div>
{% endblock %} {% endblock %}
{% block body %} {% block body %}