Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63850e48fd | |||
| f9a284cb67 | |||
| 228d320edc | |||
| 6858d2d722 | |||
| 8cd004db4a | |||
| 6cc8985c4d | |||
| 9bfd92a011 | |||
| ac6276f444 | |||
| 6d4bbf2e72 | |||
| 8004de1dc8 | |||
| 2d9a2a1f14 | |||
| b6bc1645b4 | |||
| 16023f1a26 | |||
| 725034dd4e | |||
| 234aeab120 | |||
| 044df00982 | |||
| 92015feaac | |||
| 5b704d9b36 | |||
| 4f72481829 | |||
| 4ee338e397 | |||
| 08d28d9a4f | |||
| b17313c8fb | |||
| f2b50b4f60 | |||
| 393e3ef41f | |||
| b1ccf3bf00 | |||
| 785794790c | |||
| c0f67a32ff | |||
| 325e4e14a7 | |||
| f9017297f3 | |||
| 0c4def42a2 | |||
| 3100912927 | |||
| 07553a172f | |||
| a88da4f637 |
3
.env
3
.env
@@ -38,6 +38,3 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
REDIS_HOST=redis://redis
|
||||
|
||||
MOVIES_PATH=/var/download/movies
|
||||
TVSHOWS_PATH=/var/download/tvshows
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM dunglas/frankenphp
|
||||
FROM dunglas/frankenphp:php8.4-alpine
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
@@ -11,6 +11,8 @@ RUN install-php-extensions \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
RUN apk add --no-cache wget
|
||||
|
||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
|
||||
|
||||
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||
|
||||
3
assets/bootstrap.js
vendored
3
assets/bootstrap.js
vendored
@@ -1,5 +1,6 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
import Popover from '@stimulus-components/popover'
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
app.register('popover', Popover);
|
||||
|
||||
@@ -29,6 +29,12 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
deleteDownload(data) {
|
||||
fetch(`/api/download/${data.params.id}`, {method: 'DELETE'})
|
||||
.then(res => res.json())
|
||||
.then(json => console.debug(json));
|
||||
}
|
||||
|
||||
// Add custom controller actions here
|
||||
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
title: String,
|
||||
tmdbId: String,
|
||||
imdbId: String,
|
||||
mediaType: String,
|
||||
}
|
||||
|
||||
addMovieMonitor() {
|
||||
console.log(`/monitor/movies/${this.tmdbIdValue}/${this.imdbIdValue}/${encodeURI(this.titleValue)}`)
|
||||
fetch(`/monitor/movies/${this.tmdbIdValue}/${this.imdbIdValue}/${encodeURI(this.titleValue)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log(json)
|
||||
})
|
||||
}
|
||||
}
|
||||
14
assets/controllers/monitor_list_controller.js
Normal file
14
assets/controllers/monitor_list_controller.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
deleteMonitor(data) {
|
||||
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
|
||||
.then(res => res.json())
|
||||
.then(json => console.debug(json));
|
||||
}
|
||||
}
|
||||
1
assets/icons/ic/twotone-cancel.svg
Normal file
1
assets/icons/ic/twotone-cancel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4c-4.41 0-8 3.59-8 8s3.59 8 8 8s8-3.59 8-8s-3.59-8-8-8m5 11.59L15.59 17L12 13.41L8.41 17L7 15.59L10.59 12L7 8.41L8.41 7L12 10.59L15.59 7L17 8.41L13.41 12z" opacity=".3"/><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10s10-4.47 10-10S17.53 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m3.59-13L12 10.59L8.41 7L7 8.41L10.59 12L7 15.59L8.41 17L12 13.41L15.59 17L17 15.59L13.41 12L17 8.41z"/></svg>
|
||||
|
After Width: | Height: | Size: 526 B |
1
assets/icons/line-md/circle-twotone.svg
Normal file
1
assets/icons/line-md/circle-twotone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0" stroke="currentColor" stroke-dasharray="64" stroke-dashoffset="64" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12c0 -4.97 4.03 -9 9 -9c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.15s" values="0;0.3"/><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0"/></path></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -18,6 +18,7 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
- $PWD/var/download:/var/download
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
tty: true
|
||||
@@ -34,8 +35,9 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
- $PWD/var/download:/var/download
|
||||
tty: true
|
||||
command: php /app/bin/console messenger:consume async -vv
|
||||
command: php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=10
|
||||
|
||||
|
||||
scheduler:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"ext-iconv": "*",
|
||||
"1tomany/rich-bundle": "^1.8",
|
||||
"aimeos/map": "^3.12",
|
||||
"chrisullyott/php-filesize": "^4.2",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.14",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.1",
|
||||
@@ -23,28 +24,28 @@
|
||||
"php-tmdb/api": "^4.1",
|
||||
"predis/predis": "^2.4",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/doctrine-messenger": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/filesystem": "7.2.*",
|
||||
"symfony/finder": "7.2.*",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/doctrine-messenger": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/filesystem": "7.3.*",
|
||||
"symfony/finder": "7.3.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/ldap": "7.2.*",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/ldap": "7.3.*",
|
||||
"symfony/mercure-bundle": "^0.3.9",
|
||||
"symfony/messenger": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/scheduler": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/messenger": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/scheduler": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/stimulus-bundle": "^2.24",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-icons": "^2.24",
|
||||
"symfony/ux-live-component": "^2.24",
|
||||
"symfony/ux-turbo": "^2.24",
|
||||
"symfony/ux-twig-component": "^2.24",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"symfonycasts/tailwind-bundle": "^0.10.0",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
@@ -98,13 +99,13 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.2.*"
|
||||
"require": "7.3.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*"
|
||||
}
|
||||
}
|
||||
|
||||
818
composer.lock
generated
818
composer.lock
generated
File diff suppressed because it is too large
Load Diff
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
@@ -5,9 +5,10 @@
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
# Media
|
||||
media.base_path: '/var/download'
|
||||
media.default_movies_dir: movies
|
||||
media.default_tvshows_dir: tvshows
|
||||
media.movies_path: '%env(default:media.default_movies_dir:MOVIES_PATH)%'
|
||||
media.movies_path: '/var/download/%env(default:media.default_movies_dir:MOVIES_PATH)%'
|
||||
media.tvshows_path: '/var/download/%env(default:media.default_tvshows_dir:TVSHOWS_PATH)%'
|
||||
|
||||
# Mercure
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- /mnt/media/downloads/movies:/var/download/movies
|
||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||
restart: always
|
||||
command: -vv
|
||||
command: -vv --time-limit=3600 --limit=10
|
||||
deploy:
|
||||
replicas: 2
|
||||
depends_on:
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN install-php-extensions \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
#COPY . /app
|
||||
COPY . /app
|
||||
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ RUN install-php-extensions \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
RUN apk add --no-cache wget
|
||||
|
||||
COPY . /app
|
||||
|
||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
|
||||
worker {
|
||||
file ./public/index.php
|
||||
num 20
|
||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||
{$FRANKENPHP_WORKER_CONFIG}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,7 @@ return [
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
'@stimulus-components/popover' => [
|
||||
'version' => '7.0.0',
|
||||
],
|
||||
];
|
||||
|
||||
129
nomad.deploy.hcl
Normal file
129
nomad.deploy.hcl
Normal file
@@ -0,0 +1,129 @@
|
||||
variable "image_tag" {
|
||||
type = string
|
||||
description = "Docker image tag to deploy."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
job "torsearch" {
|
||||
datacenters = [ "home" ]
|
||||
type = "service"
|
||||
|
||||
group "app" {
|
||||
count = 2
|
||||
|
||||
update {
|
||||
max_parallel = 4
|
||||
min_healthy_time = "30s"
|
||||
healthy_deadline = "3m"
|
||||
auto_revert = true
|
||||
}
|
||||
|
||||
network {
|
||||
port "app" {
|
||||
to = 80
|
||||
}
|
||||
}
|
||||
|
||||
task "app" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "registry.caldwell.digital/home/torsearch-app:${var.image_tag}"
|
||||
ports = ["app"]
|
||||
}
|
||||
|
||||
env {
|
||||
MERCURE_PUBLISHER_JWT_KEY = "!ChangeThisMercureHubJWTSecretKey!"
|
||||
MERCURE_SUBSCRIBER_JWT_KEY = "!ChangeThisMercureHubJWTSecretKey!"
|
||||
}
|
||||
|
||||
service {
|
||||
name = "torsearch-app"
|
||||
provider = "nomad"
|
||||
port = "app"
|
||||
|
||||
meta {
|
||||
nomad_ingress_enabled = true
|
||||
nomad_ingress_hostname = "torsearch-nomad.caldwell.digital"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group "worker" {
|
||||
count = 2
|
||||
update {
|
||||
max_parallel = 4
|
||||
min_healthy_time = "30s"
|
||||
healthy_deadline = "3m"
|
||||
auto_revert = true
|
||||
}
|
||||
|
||||
volume "media" {
|
||||
type = "host"
|
||||
source = "media"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
task "worker" {
|
||||
driver = "docker"
|
||||
|
||||
volume_mount {
|
||||
volume = "media"
|
||||
destination = "/var/download"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
config {
|
||||
image = "registry.caldwell.digital/home/torsearch-worker:${var.image_tag}"
|
||||
args = [
|
||||
"-vv"
|
||||
]
|
||||
}
|
||||
|
||||
service {
|
||||
name = "torsearch-worker"
|
||||
provider = "nomad"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group "scheduler" {
|
||||
count = 1
|
||||
|
||||
update {
|
||||
max_parallel = 2
|
||||
min_healthy_time = "30s"
|
||||
healthy_deadline = "3m"
|
||||
auto_revert = true
|
||||
}
|
||||
|
||||
volume "media" {
|
||||
type = "host"
|
||||
source = "media"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
task "scheduler" {
|
||||
driver = "docker"
|
||||
|
||||
volume_mount {
|
||||
volume = "media"
|
||||
destination = "/var/download"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
config {
|
||||
image = "registry.caldwell.digital/home/torsearch-scheduler:${var.image_tag}"
|
||||
args = [
|
||||
"-vv"
|
||||
]
|
||||
}
|
||||
|
||||
service {
|
||||
name = "torsearch-scheduler"
|
||||
provider = "nomad"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,25 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Util\Broadcaster;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final class AlertController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')] private readonly Environment $renderer,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/alert', name: 'app_alert')]
|
||||
public function index(): Response
|
||||
{
|
||||
$update = new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('Alert.stream.html.twig', [
|
||||
'alert_id' => 1,
|
||||
'title' => 'Added to queue',
|
||||
'message' => 'This is a testy test!',
|
||||
])
|
||||
$this->broadcaster->alert(
|
||||
'Added to queue',
|
||||
'This is a testy test!'
|
||||
);
|
||||
|
||||
$this->hub->publish($update);
|
||||
return $this->json([
|
||||
'Success' => 'Published'
|
||||
]);
|
||||
|
||||
@@ -33,11 +33,12 @@ final class SearchController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
|
||||
#[Route('/result/{mediaType}/{imdbId}', name: 'app_search_result')]
|
||||
public function result(
|
||||
GetMediaInfoInput $input,
|
||||
): Response {
|
||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||
|
||||
$this->warmDownloadOptionCache($result->media);
|
||||
|
||||
return $this->render('search/result.html.twig', [
|
||||
|
||||
@@ -6,12 +6,11 @@ use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||
use App\Util\Broadcaster;
|
||||
use Carbon\Carbon;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
@@ -21,8 +20,7 @@ final class TorrentioController extends AbstractController
|
||||
public function __construct(
|
||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly \Twig\Environment $renderer,
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
|
||||
@@ -75,14 +73,10 @@ final class TorrentioController extends AbstractController
|
||||
);
|
||||
$cache->delete($cacheId);
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderer->render('Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => 'Torrentio cache Cleared.',
|
||||
])
|
||||
));
|
||||
$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));
|
||||
|
||||
15
src/Download/Action/Command/DeleteDownloadCommand.php
Normal file
15
src/Download/Action/Command/DeleteDownloadCommand.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
/**
|
||||
* @implements CommandInterface<DeleteDownloadCommand>
|
||||
*/
|
||||
class DeleteDownloadCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $downloadId,
|
||||
) {}
|
||||
}
|
||||
26
src/Download/Action/Handler/DeleteDownloadHandler.php
Normal file
26
src/Download/Action/Handler/DeleteDownloadHandler.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Action\Handler;
|
||||
|
||||
use App\Download\Action\Command\DeleteDownloadCommand;
|
||||
use App\Download\Action\Result\DeleteDownloadResult;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements HandlerInterface<DeleteDownloadCommand, DeleteDownloadResult> */
|
||||
readonly class DeleteDownloadHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$download = $this->downloadRepository->find($command->downloadId);
|
||||
$this->downloadRepository->delete($command->downloadId);
|
||||
|
||||
return new DeleteDownloadResult(200, 'Success', $download);
|
||||
}
|
||||
}
|
||||
24
src/Download/Action/Input/DeleteDownloadInput.php
Normal file
24
src/Download/Action/Input/DeleteDownloadInput.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Action\Input;
|
||||
|
||||
use App\Download\Action\Command\DeleteDownloadCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/** @implements InputInterface<DeleteDownloadInput> */
|
||||
class DeleteDownloadInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceRoute('downloadId')]
|
||||
public int $downloadId,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new DeleteDownloadCommand(
|
||||
$this->downloadId,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Download/Action/Result/DeleteDownloadResult.php
Normal file
16
src/Download/Action/Result/DeleteDownloadResult.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Action\Result;
|
||||
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements ResultInterface<DownloadMediaResult> */
|
||||
class DeleteDownloadResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $status,
|
||||
public string $message,
|
||||
public Download $download,
|
||||
) {}
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace App\Download\Framework\Controller;
|
||||
|
||||
use App\Download\Action\Handler\DeleteDownloadHandler;
|
||||
use App\Download\Action\Input\DeleteDownloadInput;
|
||||
use App\Download\Action\Input\DownloadMediaInput;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Util\Broadcaster;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -17,12 +17,11 @@ class ApiController extends AbstractController
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
private MessageBusInterface $bus,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/api/download', name: 'api_download', methods: ['POST'])]
|
||||
public function download(
|
||||
Request $request,
|
||||
DownloadMediaInput $input,
|
||||
): Response {
|
||||
$download = $this->downloadRepository->insert(
|
||||
@@ -45,15 +44,25 @@ class ApiController extends AbstractController
|
||||
return $this->json(['error' => $exception->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderView('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => '"' . $input->title . '" added to Queue',
|
||||
])
|
||||
));
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: "$input->title added to Queue."
|
||||
);
|
||||
|
||||
return $this->json(['status' => 200, 'message' => 'Added to Queue']);
|
||||
}
|
||||
|
||||
#[Route('/api/download/{downloadId}', name: 'api_download_delete', methods: ['DELETE'])]
|
||||
public function deleteDownload(
|
||||
DeleteDownloadInput $input,
|
||||
DeleteDownloadHandler $handler,
|
||||
): Response {
|
||||
$result = $handler->handle($input->toCommand());
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: "{$result->download->getTitle()} has been deleted.",
|
||||
);
|
||||
|
||||
return $this->json(['status' => 200, 'message' => 'Download Deleted']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
namespace App\Download\Framework\Entity;
|
||||
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Torrentio\Result\ResultFactory;
|
||||
use App\Torrentio\Result\TorrentioResult;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Nihilarr\PTN;
|
||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
|
||||
@@ -162,4 +165,16 @@ class Download
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPtn(): object
|
||||
{
|
||||
$ptn = (object) (new PTN())->parse($this->filename);
|
||||
|
||||
if ($this->mediaType === "tvshows") {
|
||||
$ptn->season = str_pad($ptn->season, 2, "0", STR_PAD_LEFT);
|
||||
$ptn->episode = str_pad($ptn->episode, 2, "0", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $ptn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,27 +25,31 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
public function getCompletePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 10)
|
||||
public function getCompletePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 10, string $term = ""): Paginator
|
||||
{
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->andWhere('d.user = :user')
|
||||
->andWhere('(d.title LIKE :term OR d.imdbId LIKE :term)')
|
||||
->orderBy('d.id', 'DESC')
|
||||
->setParameter('statuses', ['Complete'])
|
||||
->setParameter('user', $user)
|
||||
->setParameter('term', '%' . $term . '%')
|
||||
->getQuery();
|
||||
|
||||
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
||||
}
|
||||
|
||||
public function getActivePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 5)
|
||||
public function getActivePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 5, string $term = ""): Paginator
|
||||
{
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->andWhere('d.user = :user')
|
||||
->andWhere('(d.title LIKE :term OR d.imdbId LIKE :term)')
|
||||
->orderBy('d.id', 'ASC')
|
||||
->setParameter('statuses', ['New', 'In Progress'])
|
||||
->setParameter('user', $user)
|
||||
->setParameter('term', '%' . $term . '%')
|
||||
->getQuery();
|
||||
|
||||
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
||||
|
||||
13
src/Monitor/Action/Command/DeleteMonitorCommand.php
Normal file
13
src/Monitor/Action/Command/DeleteMonitorCommand.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
/** @implements CommandInterface<DeleteMonitorCommand> */
|
||||
class DeleteMonitorCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $monitorId,
|
||||
) {}
|
||||
}
|
||||
36
src/Monitor/Action/Handler/DeleteMonitorHandler.php
Normal file
36
src/Monitor/Action/Handler/DeleteMonitorHandler.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use App\Monitor\Action\Command\AddMonitorCommand;
|
||||
use App\Monitor\Action\Command\DeleteMonitorCommand;
|
||||
use App\Monitor\Action\Result\AddMonitorResult;
|
||||
use App\Monitor\Action\Result\DeleteMonitorResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements HandlerInterface<DeleteMonitorCommand, DeleteMonitorResult> */
|
||||
readonly class DeleteMonitorHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MonitorRepository $monitorRepository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||
$this->monitorRepository->getEntityManager()->remove($monitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
return new DeleteMonitorResult(
|
||||
status: 'OK',
|
||||
result: [],
|
||||
monitor: $monitor
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/Monitor/Action/Input/DeleteMonitorInput.php
Normal file
24
src/Monitor/Action/Input/DeleteMonitorInput.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Input;
|
||||
|
||||
use App\Monitor\Action\Command\DeleteMonitorCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/** @implements InputInterface<DeleteMonitorInput, DeleteMonitorCommand> */
|
||||
class DeleteMonitorInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceRoute('monitorId')]
|
||||
public int $monitorId,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new DeleteMonitorCommand(
|
||||
$this->monitorId
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/Monitor/Action/Result/DeleteMonitorResult.php
Normal file
15
src/Monitor/Action/Result/DeleteMonitorResult.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Result;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class DeleteMonitorResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public array $result,
|
||||
public Monitor $monitor,
|
||||
) {}
|
||||
}
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use App\Download\Action\Input\DeleteDownloadInput;
|
||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
||||
use App\Monitor\Action\Input\AddMonitorInput;
|
||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||
use App\Util\Broadcaster;
|
||||
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\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
class ApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')]
|
||||
private readonly Environment $renderer,
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/api/monitor', name: 'api_monitor', methods: ['POST'])]
|
||||
@@ -29,12 +31,31 @@ class ApiController extends AbstractController
|
||||
$command->userId = $this->getUser()->getId();
|
||||
$response = $handler->handle($command);
|
||||
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: "New monitor added for {$input->title}",
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'status' => 200,
|
||||
'message' => $response
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/monitor/{monitorId}', name: 'api_monitor_delete', methods: ['DELETE'])]
|
||||
public function deleteMonitor(
|
||||
DeleteMonitorInput $input,
|
||||
DeleteMonitorHandler $handler,
|
||||
HubInterface $hub,
|
||||
) {
|
||||
$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 {$input->title}",
|
||||
'message' => "New monitor added for {$response->monitor->getTitle()}",
|
||||
])
|
||||
));
|
||||
|
||||
|
||||
31
src/Monitor/Framework/Controller/WebController.php
Normal file
31
src/Monitor/Framework/Controller/WebController.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use App\Download\Action\Input\DeleteDownloadInput;
|
||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
||||
use App\Monitor\Action\Input\AddMonitorInput;
|
||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
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\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
class WebController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')]
|
||||
private readonly Environment $renderer,
|
||||
) {}
|
||||
|
||||
#[Route('/monitors', name: 'app_monitors', methods: ['GET'])]
|
||||
public function addMonitor()
|
||||
{
|
||||
return $this->render('monitor/index.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ use App\User\Framework\Entity\User;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||
|
||||
#[Broadcast(template: 'broadcast/Monitor.stream.html.twig')]
|
||||
#[ORM\Entity(repositoryClass: MonitorRepository::class)]
|
||||
class Monitor
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ class MonitorRepository extends ServiceEntityRepository
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
public function getUserMonitorsPaginated(UserInterface $user, int $page, int $perPage)
|
||||
public function getUserMonitorsPaginated(UserInterface $user, int $page, int $perPage): Paginator
|
||||
{
|
||||
$query = $this->createQueryBuilder('m')
|
||||
->andWhere('m.status IN (:statuses)')
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
namespace App\Monitor\Service;
|
||||
|
||||
use Aimeos\Map;
|
||||
use Nihilarr\PTN;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class MediaFiles
|
||||
{
|
||||
private Finder $finder;
|
||||
|
||||
private string $basePath;
|
||||
|
||||
private string $moviesPath;
|
||||
|
||||
private string $tvShowsPath;
|
||||
@@ -18,6 +22,9 @@ class MediaFiles
|
||||
private Filesystem $filesystem;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(param: 'media.base_path')]
|
||||
string $basePath,
|
||||
|
||||
#[Autowire(param: 'media.movies_path')]
|
||||
string $moviesPath,
|
||||
|
||||
@@ -27,6 +34,7 @@ class MediaFiles
|
||||
Filesystem $filesystem,
|
||||
) {
|
||||
$this->finder = new Finder();
|
||||
$this->basePath = $basePath;
|
||||
$this->moviesPath = $moviesPath;
|
||||
$this->tvShowsPath = $tvShowsPath;
|
||||
$this->filesystem = $filesystem;
|
||||
@@ -43,6 +51,11 @@ class MediaFiles
|
||||
throw new \Exception(sprintf('A path for media type %s does not exist.', $mediaType));
|
||||
}
|
||||
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
public function getMoviesPath(): string
|
||||
{
|
||||
return $this->moviesPath;
|
||||
@@ -125,4 +138,49 @@ class MediaFiles
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function episodeExists(string $tvshowTitle, int $seasonNumber, int $episodeNumber)
|
||||
{
|
||||
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||
|
||||
if ($existingEpisodes->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var SplFileInfo $episode */
|
||||
foreach ($existingEpisodes as $episode) {
|
||||
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
||||
|
||||
if ($ptn->season === $seasonNumber && $ptn->episode === $episodeNumber) {
|
||||
return $episode;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function movieExists(string $title)
|
||||
{
|
||||
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
||||
$directoryExists = $this->filesystem->exists($filepath);
|
||||
|
||||
if (false === $directoryExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === $this->finder->in($filepath)->files()->hasResults()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$files = Map::from($this->finder->in($filepath)->files())->filter(function (SplFileInfo $file) {
|
||||
$validExtensions = ['mkv', 'mp4', 'mpeg'];
|
||||
return in_array($file->getExtension(), $validExtensions);
|
||||
})->values();
|
||||
|
||||
if (false === $files->isEmpty()) {
|
||||
return $files[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
class GetMediaInfoCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $tmdbId,
|
||||
public string $imdbId,
|
||||
public string $mediaType,
|
||||
) {}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class GetMediaInfoHandler implements HandlerInterface
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->tmdbId, $command->mediaType);
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
|
||||
return new GetMediaInfoResult($media);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use OneToMany\RichBundle\Contract\InputInterface;
|
||||
class GetMediaInfoInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceRoute('tmdbId')]
|
||||
public string $tmdbId,
|
||||
#[SourceRoute('imdbId')]
|
||||
public string $imdbId,
|
||||
|
||||
#[SourceRoute('mediaType')]
|
||||
public string $mediaType,
|
||||
@@ -21,6 +21,6 @@ class GetMediaInfoInput implements InputInterface
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new GetMediaInfoCommand($this->tmdbId, $this->mediaType);
|
||||
return new GetMediaInfoCommand($this->imdbId, $this->mediaType);
|
||||
}
|
||||
}
|
||||
@@ -133,9 +133,12 @@ class Tmdb
|
||||
if (!$result instanceof Movie && !$result instanceof Tv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[] = $this->parseResult($result);
|
||||
}
|
||||
|
||||
$results = array_filter($results, fn ($result) => null !== $result->imdbId);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -143,7 +146,16 @@ class Tmdb
|
||||
{
|
||||
$finder = new Find($this->client);
|
||||
$result = $finder->findBy($id, ['external_source' => 'imdb_id']);
|
||||
return $this->parseResult($result);
|
||||
|
||||
if (count($result['movie_results']) > 0) {
|
||||
return $result['movie_results'][0]['id'];
|
||||
} elseif (count($result['tv_results']) > 0) {
|
||||
return $result['tv_results'][0]['id'];
|
||||
} elseif (count($result['tv_episode_results']) > 0) {
|
||||
return $result['tv_episode_results'][0]['show_id'];
|
||||
}
|
||||
|
||||
throw new \Exception("No results found for $id");
|
||||
}
|
||||
|
||||
public function movieDetails(string $id)
|
||||
@@ -183,6 +195,8 @@ class Tmdb
|
||||
|
||||
public function mediaDetails(string $id, string $type)
|
||||
{
|
||||
$id = $this->find($id);
|
||||
|
||||
if ($type === "movies") {
|
||||
return $this->movieDetails($id);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Torrentio\Action\Handler;
|
||||
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Torrentio\Action\Result\GetMovieOptionsResult;
|
||||
use App\Torrentio\Client\Torrentio;
|
||||
@@ -14,12 +15,15 @@ class GetMovieOptionsHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly Torrentio $torrentio,
|
||||
private readonly MediaFiles $mediaFiles
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, 'movies');
|
||||
return new GetMovieOptionsResult(
|
||||
media: $this->tmdb->mediaDetails($command->tmdbId, 'movies'),
|
||||
media: $media,
|
||||
file: $this->mediaFiles->movieExists($media->title),
|
||||
results: $this->torrentio->search($command->imdbId, 'movies'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Torrentio\Action\Handler;
|
||||
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||
@@ -16,12 +17,18 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly Torrentio $torrentio,
|
||||
private readonly MediaFiles $mediaFiles,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode);
|
||||
$parentShow = $this->tmdb->mediaDetails($command->imdbId, 'tvshows');
|
||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||
|
||||
return new GetTvShowOptionsResult(
|
||||
media: $this->tmdb->episodeDetails($command->tmdbId, $command->season, $command->episode),
|
||||
media: $media,
|
||||
file: $file,
|
||||
season: $command->season,
|
||||
episode: $command->episode,
|
||||
results: $this->torrentio->fetchEpisodeResults(
|
||||
|
||||
@@ -9,6 +9,7 @@ class GetMovieOptionsResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public bool|\SplFileInfo $file,
|
||||
public array $results
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ namespace App\Torrentio\Action\Result;
|
||||
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class GetTvShowOptionsResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public bool|SplFileInfo $file,
|
||||
public string $season,
|
||||
public string $episode,
|
||||
public array $results
|
||||
|
||||
@@ -16,15 +16,14 @@ final class DownloadList extends AbstractController
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
|
||||
use PaginateTrait;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public string $term = "";
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public string $type;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public int $pageNumber = 1;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public int $perPage = 5;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public bool $isWidget = true;
|
||||
|
||||
@@ -35,17 +34,11 @@ final class DownloadList extends AbstractController
|
||||
public function getDownloads()
|
||||
{
|
||||
if ($this->type === "active") {
|
||||
return $this->downloadRepository->getActivePaginated($this->getUser(), $this->pageNumber, $this->perPage);
|
||||
return $this->downloadRepository->getActivePaginated($this->getUser(), $this->pageNumber, $this->perPage, $this->term);
|
||||
} elseif ($this->type === "complete") {
|
||||
return $this->downloadRepository->getCompletePaginated($this->getUser(), $this->pageNumber, $this->perPage);
|
||||
return $this->downloadRepository->getCompletePaginated($this->getUser(), $this->pageNumber, $this->perPage, $this->term);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function paginate(#[LiveArg] int $page)
|
||||
{
|
||||
$this->pageNumber = $page;
|
||||
}
|
||||
}
|
||||
|
||||
14
src/Twig/Components/DownloadListRow.php
Normal file
14
src/Twig/Components/DownloadListRow.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent]
|
||||
final class DownloadListRow
|
||||
{
|
||||
public Download $download;
|
||||
|
||||
public bool $isWidget = true;
|
||||
}
|
||||
@@ -3,14 +3,24 @@
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use Doctrine\ORM\Query\Expr\OrderBy;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveArg;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
|
||||
#[AsTwigComponent]
|
||||
#[AsLiveComponent]
|
||||
final class MonitorList extends AbstractController
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
|
||||
use PaginateTrait;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public string $type;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public bool $isWidget = true;
|
||||
|
||||
public function __construct(
|
||||
@@ -18,8 +28,36 @@ final class MonitorList extends AbstractController
|
||||
) {}
|
||||
|
||||
#[LiveAction]
|
||||
public function getUserMonitors(#[LiveArg] int $page = 1, #[LiveArg] int $perPage = 5)
|
||||
public function getMonitors()
|
||||
{
|
||||
return $this->monitorRepository->getUserMonitorsPaginated($this->getUser(), $page, $perPage);
|
||||
if ($this->type === "active") {
|
||||
return $this->getActiveUserMonitors();
|
||||
} elseif ($this->type === "complete") {
|
||||
return $this->getCompleteUserMonitors();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function getActiveUserMonitors()
|
||||
{
|
||||
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||
->andWhere('m.status IN (:statuses)')
|
||||
->setParameter('statuses', ['New', 'In Progress'])
|
||||
->orderBy('m.id', 'DESC')
|
||||
->getQuery()
|
||||
);
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function getCompleteUserMonitors()
|
||||
{
|
||||
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||
->andWhere('m.status = :status')
|
||||
->setParameter('status', 'Complete')
|
||||
->orderBy('m.id', 'DESC')
|
||||
->getQuery()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
12
src/Twig/Components/MonitorListRow.php
Normal file
12
src/Twig/Components/MonitorListRow.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent]
|
||||
final class MonitorListRow
|
||||
{
|
||||
public Monitor $monitor;
|
||||
}
|
||||
30
src/Twig/Components/PaginateTrait.php
Normal file
30
src/Twig/Components/PaginateTrait.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Util\Paginator;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveArg;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||
|
||||
trait PaginateTrait
|
||||
{
|
||||
#[LiveProp(writable: true)]
|
||||
public int $pageNumber = 1;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public int $perPage = 5;
|
||||
|
||||
#[LiveAction]
|
||||
public function paginate(#[LiveArg] int $page)
|
||||
{
|
||||
$this->pageNumber = $page;
|
||||
}
|
||||
|
||||
private function asPaginator(Query $query): Paginator
|
||||
{
|
||||
return (new Paginator())
|
||||
->paginate($query, $this->pageNumber, $this->perPage);
|
||||
}
|
||||
}
|
||||
36
src/Twig/Extensions/MonitorExtension.php
Normal file
36
src/Twig/Extensions/MonitorExtension.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extensions;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
|
||||
class MonitorExtension
|
||||
{
|
||||
#[AsTwigFilter('monitor_type')]
|
||||
public function type(string $type)
|
||||
{
|
||||
$types = [
|
||||
'tvshows' => 'Show',
|
||||
'tvseason' => 'Season',
|
||||
'tvepisode' => 'Episode',
|
||||
];
|
||||
|
||||
return $types[$type] ?? '-';
|
||||
}
|
||||
|
||||
#[AsTwigFilter('monitor_media_id')]
|
||||
public function mediaId(Monitor $monitor)
|
||||
{
|
||||
if ($monitor->getMonitorType() === "tvseason") {
|
||||
return "S". str_pad($monitor->getSeason(), 2, "0", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
if ($monitor->getMonitorType() === "tvepisode") {
|
||||
return "S". str_pad($monitor->getSeason(), 2, "0", STR_PAD_LEFT) .
|
||||
"E". str_pad($monitor->getEpisode(), 2, "0", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
32
src/Twig/Extensions/UtilExtension.php
Normal file
32
src/Twig/Extensions/UtilExtension.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extensions;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use ChrisUllyott\FileSize;
|
||||
use Twig\Attribute\AsTwigFilter;
|
||||
|
||||
class UtilExtension
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly MediaFiles $mediaFiles,
|
||||
) {}
|
||||
|
||||
#[AsTwigFilter('filesize')]
|
||||
public function type(string|int $size)
|
||||
{
|
||||
return (new FileSize($size))->asAuto();
|
||||
}
|
||||
|
||||
#[AsTwigFilter('strip_media_path')]
|
||||
public function stripMediaPath(string $path)
|
||||
{
|
||||
return str_replace(
|
||||
$this->mediaFiles->getBasePath() . DIRECTORY_SEPARATOR,
|
||||
'',
|
||||
$path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,20 @@ use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
||||
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use App\Util\Broadcaster;
|
||||
use App\Util\CountryLanguages;
|
||||
use App\Util\ProviderList;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class PreferencesController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PreferencesRepository $preferencesRepository,
|
||||
private readonly SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly SaveUserDownloadPreferencesHandler $saveUserDownloadPreferencesHandler,
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/user/preferences', 'app_user_preferences', methods: ['GET'])]
|
||||
public function mediaPreferences(): Response
|
||||
{
|
||||
@@ -48,25 +45,21 @@ class PreferencesController extends AbstractController
|
||||
|
||||
#[Route('/user/preferences/media', 'app_save_media_preferences', methods: ['POST'])]
|
||||
public function saveMediaPreferences(
|
||||
Request $request,
|
||||
SaveUserMediaPreferencesInput $input,
|
||||
SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler,
|
||||
): Response
|
||||
{
|
||||
$this->saveUserMediaPreferencesHandler->handle($input->toCommand());
|
||||
$saveUserMediaPreferencesHandler->handle($input->toCommand());
|
||||
$mediaPreferences = $this->getUser()->getMediaPreferences();
|
||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||
|
||||
$languages = CountryLanguages::$languages;
|
||||
sort($languages);
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderView('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => 'Your media preferences have been saved.',
|
||||
])
|
||||
));
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: 'Your media preferences have been saved.'
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
@@ -82,24 +75,20 @@ class PreferencesController extends AbstractController
|
||||
|
||||
#[Route('/user/preferences/download', 'app_save_download_preferences', methods: ['POST'])]
|
||||
public function saveDownloadPreferences(
|
||||
Request $request,
|
||||
SaveUserDownloadPreferencesInput $input,
|
||||
SaveUserDownloadPreferencesHandler $saveUserDownloadPreferencesHandler,
|
||||
): Response
|
||||
{
|
||||
$downloadPreferences = $this->saveUserDownloadPreferencesHandler->handle($input->toCommand())->downloadPreferences;
|
||||
$downloadPreferences = $saveUserDownloadPreferencesHandler->handle($input->toCommand())->downloadPreferences;
|
||||
$mediaPreferences = $this->getUser()->getMediaPreferences();
|
||||
|
||||
$languages = CountryLanguages::$languages;
|
||||
sort($languages);
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderView('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => 'Your download preferences have been saved.',
|
||||
])
|
||||
));
|
||||
$this->broadcaster->alert(
|
||||
title: 'Success',
|
||||
message: 'Your download preferences have been saved.'
|
||||
);
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
|
||||
33
src/Util/Broadcaster.php
Normal file
33
src/Util/Broadcaster.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Twig\Environment;
|
||||
|
||||
readonly class Broadcaster
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')]
|
||||
private Environment $renderer,
|
||||
private HubInterface $hub,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function alert(string $title, string $message): void
|
||||
{
|
||||
$userAlertTopic = $this->requestStack->getCurrentRequest()->getSession()->get('mercure_alert_topic');
|
||||
$update = new Update(
|
||||
$userAlertTopic,
|
||||
$this->renderer->render('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
])
|
||||
);
|
||||
$this->hub->publish($update);
|
||||
}
|
||||
}
|
||||
21
symfony.lock
21
symfony.lock
@@ -2,6 +2,15 @@
|
||||
"1tomany/rich-bundle": {
|
||||
"version": "v1.8.3"
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||
}
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.14",
|
||||
"recipe": {
|
||||
@@ -169,6 +178,18 @@
|
||||
"config/packages/messenger.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/property_info.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
|
||||
@@ -4,8 +4,33 @@ module.exports = {
|
||||
"./assets/**/*.js",
|
||||
"./templates/**/*.html.twig",
|
||||
],
|
||||
safelist: [
|
||||
"bg-blue-300",
|
||||
"bg-orange-300",
|
||||
"bg-fuchsia-300",
|
||||
"bg-green-400",
|
||||
"bg-purple-400",
|
||||
"bg-orange-400",
|
||||
"bg-blue-600",
|
||||
"bg-rose-600",
|
||||
"transition-opacity",
|
||||
"ease-in",
|
||||
"duration-700",
|
||||
"opacity-100"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
animation: {
|
||||
fade: 'fadeIn .3s ease-in-out',
|
||||
},
|
||||
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
from: { opacity: 0 },
|
||||
to: { opacity: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -5,36 +5,19 @@
|
||||
|
||||
<turbo-stream action="append" target="active_downloads">
|
||||
<template>
|
||||
<tr data-download-list-target="download" id="ad_download_{{ entity.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ entity.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
|
||||
{% if entity.progress < 100 %}
|
||||
<div class="w-[3.25ch] h-[3.25ch] bg-purple-600 rounded-full block text-center table-cell align-middle text-xs text-gray-50">
|
||||
{{ entity.progress }}
|
||||
</div>
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="Complete" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<twig:DownloadListRow download="{{ entity }}" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
{% block update %}
|
||||
{% if entity.status != "Complete" %}
|
||||
<turbo-stream action="update" target="ad_download_{{ id }}">
|
||||
<turbo-stream action="update" target="download_progress_{{ id }}">
|
||||
<template>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ entity.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<div class="border-2 border-green-700 rounded-md w-full h-6 align-middle overflow-hidden">
|
||||
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5" style="width:{{ entity.progress }}%">{{ entity.progress }}%</div>
|
||||
</div>
|
||||
</td>
|
||||
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5"
|
||||
style="width:{{ entity.progress }}%">
|
||||
<span>{{ entity.progress }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% else %}
|
||||
@@ -52,14 +35,7 @@
|
||||
|
||||
<turbo-stream action="prepend" target="complete_downloads">
|
||||
<template>
|
||||
<tr id="ad_download_{{ entity.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ entity.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
|
||||
<twig:StatusBadge color="green" status="Complete" />
|
||||
</td>
|
||||
</tr>
|
||||
<twig:DownloadListRow download="{{ entity }}" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endif %}
|
||||
|
||||
29
templates/broadcast/Monitor.stream.html.twig
Normal file
29
templates/broadcast/Monitor.stream.html.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
{# Learn how to use Turbo Streams: https://github.com/symfony/ux-turbo#broadcast-doctrine-entities-update #}
|
||||
{% block create %}
|
||||
<turbo-stream action="remove" target="active_monitors_no_monitors">
|
||||
</turbo-stream>
|
||||
|
||||
<turbo-stream action="prepend" target="monitors">
|
||||
<template>
|
||||
<twig:MonitorListRow monitor="{{ entity }}" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
{% block update %}
|
||||
<turbo-stream action="replace" target="monitor_{{ id }}">
|
||||
<template>
|
||||
<twig:MonitorListRow monitor="{{ entity }}" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
{% block remove %}
|
||||
<turbo-stream action="remove" target="monitor_{{ id }}"></turbo-stream>
|
||||
|
||||
<turbo-stream action="prepend" target="alert_list">
|
||||
<template>
|
||||
<twig:Alert title="Success" message="Your Monitor for '{{ entity.title }}' has been removed." alert_id="monitor_alert_{{ entity.id }}" data-controller="alert" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,12 @@
|
||||
<div{{ attributes.defaults(stimulus_controller('download_list')) }} class="min-w-48" >
|
||||
{% set table_body_id = (type == "complete") ? "complete_downloads" : "active_downloads" %}
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
<div class="flex flex-row mb-2 justify-end">
|
||||
<twig:DownloadSearch search_path="app_search" placeholder="Find one of your downloads..." />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table id="downloads" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
|
||||
<thead>
|
||||
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
|
||||
@@ -23,42 +30,19 @@
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
|
||||
Progress
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="{{ table_body_id }}" class="divide-y divide-gray-200 dark:divide-gray-50">
|
||||
{% if this.downloads.items|length > 0 %}
|
||||
{% for download in this.downloads.items %}
|
||||
<tr id="ad_download_{{ download.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate">
|
||||
{{ download.title }}
|
||||
</td>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
{{ download.filename }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
{{ download.mediaType }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
|
||||
{% if download.progress < 100 %}
|
||||
<div class="border-2 border-green-700 rounded-md w-full h-6 align-middle overflow-hidden">
|
||||
<div class="text-green-700 rounded-sm text-bold text-gray-950 text-center bg-green-600 h-5" style="width:{{ download.progress }}%">{{ download.progress }}%</div>
|
||||
</div>
|
||||
{# <div class="w-[3.25ch] h-[3.25ch] bg-purple-600 rounded-full block text-center table-cell align-middle text-xs text-gray-50">#}
|
||||
{# {{ download.progress }}#}
|
||||
{# </div>#}
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="Completed" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<twig:DownloadListRow download="{{ download }}" isWidget="{{ this.isWidget }}" />
|
||||
{% endfor %}
|
||||
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
|
||||
<tr id="download_view_all">
|
||||
<td class="py-2 whitespace-nowrap bg-orange-500 uppercase text-sm font-medium text-center text-white truncate" colspan="100%">
|
||||
<td class="py-2 whitespace-nowrap bg-orange-300 uppercase text-xs font-medium text-center text-black truncate" colspan="100%">
|
||||
<a href="{{ path('app_downloads') }}">View All Downloads</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
44
templates/components/DownloadListRow.html.twig
Normal file
44
templates/components/DownloadListRow.html.twig
Normal file
@@ -0,0 +1,44 @@
|
||||
<tr{{ attributes }} id="ad_download_{{ download.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
|
||||
class="hover:underline mr-1"
|
||||
>
|
||||
{{ download.title }}
|
||||
</a>
|
||||
|
||||
{% if download.mediaType == "tvshows" %}
|
||||
— <span class="ml-1">(S{{ download.ptn.season }}E{{ download.ptn.episode }})</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if isWidget == false %}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] truncate">
|
||||
{{ download.filename }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
{{ download.mediaType }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
|
||||
{% if download.progress < 100 %}
|
||||
<div id="download_progress_{{ download.id }}" class="border-2 border-green-600 rounded-md text-center w-full h-6 align-middle overflow-hidden">
|
||||
<div class="text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
||||
style="width:{{ download.progress }}%">
|
||||
</div>
|
||||
<div class="absolute text-black text-center" style="z-index: 400;margin-top: -1.25rem; margin-left: 1.2rem">{{ download.progress }}%</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="Complete" />
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 flex flex-row align-middle justify-center">
|
||||
<button {{ stimulus_action('download_list', 'deleteDownload', 'click', {id: download.id}) }}>
|
||||
<twig:ux:icon
|
||||
name="ic:twotone-cancel" width="18px"
|
||||
class="rounded-full align-middle text-red-600"
|
||||
title="Remove {{ download.title }} from your Download list. This will not delete the file."
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
29
templates/components/DownloadSearch.html.twig
Normal file
29
templates/components/DownloadSearch.html.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
<div {{ attributes }} class="w-full max-w-sm min-w-[200px]">
|
||||
<div class="relative">
|
||||
<form>
|
||||
<input
|
||||
data-model="term"
|
||||
class="w-full bg-transparent
|
||||
placeholder:text-slate-200 text-gray-50
|
||||
text-sm border-b border-orange-500 pl-3 pr-28 py-2 transition
|
||||
duration-300 ease focus:outline-none focus:border-orange-400 hover:border-orange-300
|
||||
shadow-sm focus:shadow"
|
||||
placeholder="{{ placeholder ?? 'TV Show, Movie...' }}"
|
||||
/>
|
||||
<button
|
||||
class="absolute top-1 right-1 flex items-center
|
||||
bg-green-600 py-1 px-2 text-center
|
||||
text-md text-white transition-all
|
||||
focus:bg-green-700 active:bg-green-700 hover:bg-green-700
|
||||
|
||||
text-white bg-green-600 text-xs
|
||||
rounded-sm
|
||||
bg-opacity-80
|
||||
"
|
||||
onclick="return false;"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +1,15 @@
|
||||
<div{{ attributes }} class="min-w-48">
|
||||
<p class="text-white mb-1">The items you're currently monitoring to automatically download.</p>
|
||||
<table id="downloads" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-fixed" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
|
||||
<div{{ attributes.defaults(stimulus_controller('monitor_list')) }}>
|
||||
<table id="monitor_list" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Monitor\\Framework\\Entity\\Monitor') }}>
|
||||
<thead>
|
||||
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase min-w-[45ch] max-w-[45ch] truncate">
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase truncate">
|
||||
Title
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
ID
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Search Count
|
||||
@@ -19,49 +22,32 @@
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Last Search Date
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Type
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
{% if this.userMonitors.items|length > 0 %}
|
||||
{% for monitor in this.userMonitors.items %}
|
||||
<tr id="monitor_{{ monitor.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 min-w-[50ch] max-w-[50ch] truncate">
|
||||
{{ monitor.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.searchCount }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.createdAt|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{% if monitor.status == "New" %}
|
||||
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
||||
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
||||
<twig:StatusBadge color="purple" status="{{ monitor.status }}" />
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tbody id="monitors" class="divide-y divide-gray-50">
|
||||
{% if this.monitors.items|length > 0 %}
|
||||
{% for monitor in this.monitors.items %}
|
||||
<twig:MonitorListRow :monitor="monitor" />
|
||||
{% endfor %}
|
||||
{% if this.userMonitors.items|length > 5 %}
|
||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||
<tr id="monitor_view_all">
|
||||
<td colspan="5" class="py-2 whitespace-nowrap bg-orange-500 uppercase text-sm font-medium text-center text-white min-w-[50ch] max-w-[50ch] truncate">
|
||||
<a href="#">View All Monitors</a>
|
||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-300 uppercase text-xs font-medium text-center text-black min-w-[50ch] max-w-[50ch] truncate">
|
||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-stone-800" colspan="2">
|
||||
<tr id="active_monitors_no_monitors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-stone-800" colspan="100%">
|
||||
No monitors
|
||||
</td>
|
||||
</tr>
|
||||
@@ -70,8 +56,8 @@
|
||||
</table>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
{% if this.userMonitors.items|length > 0 %}
|
||||
{% set paginator = this.userMonitors %}
|
||||
{% if this.monitors.items|length > 0 %}
|
||||
{% set paginator = this.monitors %}
|
||||
{% include 'partial/paginator.html.twig' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
44
templates/components/MonitorListRow.html.twig
Normal file
44
templates/components/MonitorListRow.html.twig
Normal file
@@ -0,0 +1,44 @@
|
||||
<tr{{ attributes }} id="monitor_{{ monitor.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
|
||||
{{ monitor.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor|monitor_media_id }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.searchCount }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.createdAt|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{% if monitor.monitorType == "tvshow" %}
|
||||
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||
{% elseif monitor.monitorType == "tvseason" %}
|
||||
<twig:StatusBadge color="orange" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||
{% else %}
|
||||
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{% if monitor.status == "New" %}
|
||||
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
||||
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
||||
<twig:StatusBadge color="purple" status="{{ monitor.status }}" />
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 flex flex-row align-middle justify-center">
|
||||
<button {{ stimulus_action('monitor_list', 'deleteMonitor', 'click', {id: monitor.id}) }}>
|
||||
<twig:ux:icon
|
||||
name="ic:twotone-cancel" width="18px"
|
||||
class="rounded-full align-middle text-red-600"
|
||||
title="Remove {{ monitor.title }} from your Monitor list."
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -20,6 +20,15 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ path('app_monitors') }}"
|
||||
class="block rounded-lg
|
||||
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
||||
px-4 py-2 text-sm font-medium text-gray-50">
|
||||
Monitors
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ path('app_user_preferences') }}"
|
||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div{{ attributes }}>
|
||||
<a href="{{ path('app_search_result', {
|
||||
mediaType: mediaType,
|
||||
tmdbId: tmdbId
|
||||
imdbId: imdbId
|
||||
}) }}">
|
||||
<img src="{{ image }}" class="w-40 rounded-md" />
|
||||
</a>
|
||||
<a href="{{ path('app_search_result', {
|
||||
mediaType: mediaType,
|
||||
tmdbId: tmdbId
|
||||
imdbId: imdbId
|
||||
}) }}">
|
||||
<h3 class="text-center text-gray-50 max-w-[16ch] text-extrabold">{{ title }}</h3>
|
||||
</a>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<a class="h-9 rounded-md py-1 px-2 bg-green-600 text-gray-50"
|
||||
href="{{ path('app_search_result', {mediaType: mediaType, tmdbId: tmdbId}) }}"
|
||||
href="{{ path('app_search_result', {mediaType: mediaType, imdbId: imdbId}) }}"
|
||||
>choose</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<span {{ attributes }} class="py-[3px] px-[7px] bg-{{ color|default('green') }}-600 rounded-lg inline-block text-center text-xs text-white">
|
||||
<span {{ attributes }}
|
||||
class="py-[3px] px-[7px]
|
||||
bg-{{ color|default('green') }}-{{ number|default(400) }}
|
||||
text-{{ text|default('black') }}
|
||||
rounded-lg inline-block text-center text-xs"
|
||||
>
|
||||
{{ status }}
|
||||
</span>
|
||||
@@ -16,13 +16,13 @@
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
<twig:Card title="Monitors" class="w-full">
|
||||
<twig:MonitorList />
|
||||
<twig:MonitorList :type="'active'" :isWidget="true" />
|
||||
</twig:Card>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<twig:Card title="Popular Movies" contentClass="flex flex-row justify-between w-full">
|
||||
{% for movie in popular_movies %}
|
||||
<twig:Poster imdbId=""
|
||||
<twig:Poster imdbId="{{ movie.imdbId }}"
|
||||
tmdbId="{{ movie.tmdbId }}"
|
||||
title="{{ movie.title }}"
|
||||
description="{{ movie.description }}"
|
||||
@@ -34,7 +34,7 @@
|
||||
</twig:Card>
|
||||
<twig:Card title="Popular TV Shows" contentClass="flex flex-row justify-between w-full">
|
||||
{% for movie in popular_tvshows %}
|
||||
<twig:Poster imdbId=""
|
||||
<twig:Poster imdbId="{{ movie.imdbId }}"
|
||||
tmdbId="{{ movie.tmdbId }}"
|
||||
title="{{ movie.title }}"
|
||||
description="{{ movie.description }}"
|
||||
|
||||
18
templates/monitor/index.html.twig
Normal file
18
templates/monitor/index.html.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Monitors — Torsearch{% endblock %}
|
||||
{% block h2 %}Monitors{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4">
|
||||
<twig:Card title="Active Monitors">
|
||||
<twig:MonitorList :type="'active'" :isWidget="false" :perPage="10"></twig:MonitorList>
|
||||
</twig:Card>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<twig:Card title="Complete Monitors">
|
||||
<twig:MonitorList :type="'complete'" :isWidget="false" :perPage="10"></twig:MonitorList>
|
||||
</twig:Card>
|
||||
</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">
|
||||
{% 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">
|
||||
<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">
|
||||
{{ include('torrentio/partial/option-table.html.twig', {controller: 'movie-results'}) }}
|
||||
</div>
|
||||
|
||||
@@ -8,23 +8,59 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-col gap-4 grow">
|
||||
<h4 class="text-md font-bold">{{ results.episode }}. {{ results.media.title }}</h4>
|
||||
<h4 class="text-md font-bold">
|
||||
{{ results.episode }}. {{ results.media.title }}
|
||||
</h4>
|
||||
<p>{{ results.media.description }}</p>
|
||||
<div>
|
||||
<small class="py-1 px-1.5 grow-0 font-bold bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"
|
||||
{{ stimulus_action('tv-results', 'toggleList', 'click') }}
|
||||
><span {{ stimulus_target('tv-results', 'count') }}>{{ results.results|length }}</span> results</small>
|
||||
<small class="py-1 px-1.5 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ results.media.episodeAirDate }}"
|
||||
>{{ results.media.episodeAirDate }}</small>
|
||||
>
|
||||
<span {{ stimulus_target('tv-results', 'count') }}>{{ results.results|length }}</span> results
|
||||
</small>
|
||||
|
||||
{% if results.file != false %}
|
||||
<span data-controller="popover">
|
||||
<template data-popover-target="content">
|
||||
<div data-popover-target="card" class="absolute z-40 p-1 bg-stone-400 p-1 text-black rounded-md m-1 animate-fade">
|
||||
<p class="font-bold text-sm text-left">Existing file(s) for this episode:</p>
|
||||
<ul class="list-disc ml-3">
|
||||
<li class="font-normal">{{ results.file.realPath|strip_media_path }} — <strong>{{ results.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 results.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.">
|
||||
missing
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ results.media.episodeAirDate }}">
|
||||
{{ results.media.episodeAirDate }}
|
||||
</small>
|
||||
{# <small class="py-1 px-1.5 grow-0 font-bold bg-red-600 hover:bg-red-700 rounded-lg font-normal text-white cursor-pointer" title="Clear cache for {{ results.media.title }}"#}
|
||||
{# {{ stimulus_action('tv-results', 'clearCache', 'click') }}#}
|
||||
{# >Clear Cache</small>#}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 justify-between">
|
||||
<input type="checkbox"
|
||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||
/>
|
||||
<div class="flex flex-col items-center">
|
||||
<input type="checkbox"
|
||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||
/>
|
||||
<span title="You have this downloaded!">
|
||||
<twig:ux:icon width="20" class="mt-2 text-green-600" name="line-md:circle-twotone" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-end hover:cursor-pointer"
|
||||
{{ stimulus_action('tv-results', 'toggleList', 'click') }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 32 32">
|
||||
|
||||
Reference in New Issue
Block a user