Compare commits

...

11 Commits

33 changed files with 527 additions and 88 deletions

7
.env
View File

@@ -51,3 +51,10 @@ OIDC_CLIENT_ID="Enter your OIDC client id"
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
OIDC_BYPASS_FORM_LOGIN=false
###< drenso/symfony-oidc-bundle ###
###> symfony/ntfy-notifier ###
# NTFY_DSN=ntfy://default/TOPIC
###< symfony/ntfy-notifier ###
NOTIFICATION_TRANSPORT=
NTFY_DNS=

3
assets/bootstrap.js vendored
View File

@@ -1,7 +1,9 @@
import '@ungap/custom-elements'
import PreviewContentDialog from "./components/preview-content-dialog.js";
import EpisodeContainer from './components/episode-container.js';
import DownloadOptionTr from './components/download-option-tr.js';
import DownloadListRow from './components/download-list-row.js';
import MonitorListRow from './components/monitor-list-row.js';
import MovieContainer from "./components/movie-container.js";
import { startStimulusApp } from '@symfony/stimulus-bundle';
@@ -21,3 +23,4 @@ customElements.define('episode-container', EpisodeContainer);
customElements.define('movie-container', MovieContainer);
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'});

View File

@@ -24,11 +24,12 @@ export default class DownloadListRow extends HTMLTableRowElement {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
previewContent() {
return `
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">

View File

@@ -0,0 +1,115 @@
export default class MonitorListRow extends HTMLTableRowElement {
constructor() {
super();
this.downloadId = this.getAttribute('monitor-id');
this.imdbId = this.getAttribute('imdb-id');
this.mediaTitle = this.getAttribute('media-title');
this.url = this.getAttribute('url');
this.filename = this.getAttribute('filename');
this.status = this.getAttribute('status');
this.progress = this.getAttribute('progress');
this.mediaType = this.getAttribute('media-type');
this.episodeId = this.getAttribute('episode-id');
this.createdAt = this.getAttribute('created-at');
this.updatedAt = this.getAttribute('updated-at');
}
static get observedAttributes() {
return ['download-id', 'imdb-id', 'media-title', 'url', 'filename', 'status', 'progress', 'media-type', 'episode-id', 'created-at', 'updated-at'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">IMDB ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Title</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Season</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Status</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Search Count</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Media Type</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Created At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Updated At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
</th>
</tr>
</thead>
<tbody>
<tr class="flex flex-col">
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('monitor-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('imdb-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-title') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('season') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('status') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('search-count') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-type') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('created-at') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('last-search') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
</td>
</tr>
</tbody>
</table>
`;
}
}

View File

@@ -12,11 +12,11 @@ export default class PreviewContentDialog extends HTMLDialogElement {
this.setHeading = this.setHeading.bind(this);
this.setContent = this.setContent.bind(this);
this.#closeBtnEl.addEventListener('click', () => this.close());
document.addEventListener('hidePreviewContentModal', () => this.close());
document.addEventListener('showPreviewContentModal', (event) => {
this.display(event.detail);
});
document.addEventListener('hidePreviewContentModal', (e) => this.close());
this.#closeBtnEl.addEventListener('click', () => this.close());
}
setHeading(heading) {

View File

@@ -41,23 +41,20 @@ export default class extends Controller {
downloadTargetConnected(target) {
let downloads = this.element.querySelectorAll('tbody tr');
console.log(target)
downloads.forEach(download => {
console.log(download)
download.mediaTitle = download.getAttribute('media-title');
download.addEventListener('click', (event) => {
// let previewContentModal = document.querySelector('#previewContentModal');
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent();
heading = event.target.parentElement.mediaTitle;
content = event.target.parentElement.previewContent;
heading = "Download # " + event.target.parentElement.downloadId + " - \"" + event.target.parentElement.mediaTitle + "\"";
} else {
content = event.target.previewContent();
heading = event.target.mediaTitle;
content = event.target.previewContent;
heading = "Download # " + event.target.downloadId + " - \"" + event.target.mediaTitle + "\"";
}
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}

View File

@@ -6,6 +6,29 @@ import { Controller } from '@hotwired/stimulus';
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['monitorList']
monitorListTargetConnected(target) {
let monitors = this.element.querySelectorAll('tbody tr');
monitors.forEach(monitor => {
monitor.addEventListener('click', (event) => {
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent;
heading = "Monitor for \"" + event.target.parentElement.mediaTitle+ "\"";
} else {
content = event.target.previewContent;
heading = "Monitor for \"" + event.target.mediaTitle + "\"";
}
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}
deleteMonitor(data) {
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
.then(res => res.json())

View File

@@ -55,6 +55,10 @@ dialog {
}
}
dialog[open] {
animation: fade-in 100ms ease-in forwards;
}
/* Add animations */
dialog[data-dialog-target="dialog"][open] {
animation: fade-in 200ms forwards;
@@ -133,7 +137,7 @@ dialog[data-dialog-target="dialog"][closing] {
#search .ts-dropdown {
background: unset;
@apply bg-orange-500/80 backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md
@apply bg-orange-500/80 backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md z-20
}
#search .ts-dropdown .ts-dropdown-content .option.active {

View File

@@ -43,6 +43,8 @@
"symfony/mailer": "7.3.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "7.3.*",
"symfony/notifier": "7.3.*",
"symfony/ntfy-notifier": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/scheduler": "7.3.*",
"symfony/security-bundle": "7.3.*",

148
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f4953f7b233a466d94269f35096f3385",
"content-hash": "6f57ba35ae317ec6370836bda0012db8",
"packages": [
{
"name": "1tomany/rich-bundle",
@@ -7578,6 +7578,152 @@
],
"time": "2025-02-19T08:51:26+00:00"
},
{
"name": "symfony/notifier",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/notifier.git",
"reference": "9e68a3266c8b0381f8756022b1c1ba3c0264416e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/notifier/zipball/9e68a3266c8b0381f8756022b1c1ba3c0264416e",
"reference": "9e68a3266c8b0381f8756022b1c1ba3c0264416e",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/event-dispatcher": "<6.4",
"symfony/event-dispatcher-contracts": "<2.5",
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<6.4"
},
"require-dev": {
"symfony/event-dispatcher-contracts": "^2.5|^3",
"symfony/http-client-contracts": "^2.5|^3",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Sends notifications via one or more channels (email, SMS, ...)",
"homepage": "https://symfony.com",
"keywords": [
"notification",
"notifier"
],
"support": {
"source": "https://github.com/symfony/notifier/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-01T12:12:53+00:00"
},
{
"name": "symfony/ntfy-notifier",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ntfy-notifier.git",
"reference": "094154ba36eac54078a71076effe46feaec59036"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ntfy-notifier/zipball/094154ba36eac54078a71076effe46feaec59036",
"reference": "094154ba36eac54078a71076effe46feaec59036",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/clock": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^7.3"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Ntfy\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mickael Perraud",
"email": "mikaelkael.fr@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Ntfy Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"Ntfy",
"notifier"
],
"support": {
"source": "https://github.com/symfony/ntfy-notifier/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-02-13T10:27:54+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v7.3.0",

View File

@@ -0,0 +1,13 @@
framework:
notifier:
chatter_transports:
texter_transports:
ntfy: '%notification.ntfy.dsn%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -44,6 +44,10 @@ parameters:
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
# Notifications
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
notification.ntfy.dsn: '%env(NTFY_DSN)%'
services:
# default configuration for services in *this* file
_defaults:

View File

@@ -6,7 +6,6 @@ services:
environment:
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
tty: true
deploy:
replicas: 2
volumes:
@@ -16,6 +15,10 @@ services:
- mercure_config:/config
depends_on:
- database
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12202"
worker:
@@ -29,6 +32,10 @@ services:
replicas: 2
depends_on:
- app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12203"
scheduler:
@@ -40,6 +47,11 @@ services:
command: -vv
depends_on:
- app
logging:
driver: "gelf"
options:
gelf-address: "tcp://192.168.1.197:12204"
redis:

View File

@@ -67,4 +67,7 @@ return [
'pulltorefreshjs' => [
'version' => '0.1.22',
],
'@ungap/custom-elements' => [
'version' => '1.3.0',
],
];

View File

@@ -38,6 +38,12 @@ final class ConfigResolver
#[Autowire(param: 'auth.oidc.bypass_form_login')]
private ?bool $authOidcBypassFormLogin = null,
#[Autowire(param: 'notification.transport')]
private ?string $notificationTransport = null,
#[Autowire(param: 'notification.ntfy.dsn')]
private ?string $notificationNtfyDsn = null,
) {}
public function validate(): bool
@@ -54,6 +60,12 @@ final class ConfigResolver
$valid = false;
}
if (null !== $this->notificationTransport) {
if (null === $this->notificationNtfyDsn || "" === $this->notificationNtfyDsn) {
$this->messages[] = "Your NOTIFICATION_TRANSPORT is set to 'ntfy' but you don't have the NTFY_DSN environment variable set.";
}
}
return $valid;
}

View File

@@ -2,33 +2,53 @@
namespace App\Base\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\NotifierInterface;
use Twig\Environment;
readonly class Broadcaster
{
public function __construct(
#[Autowire(param: 'notification.transport')]
private string $notificationTransport,
#[Autowire(service: 'twig')]
private Environment $renderer,
private HubInterface $hub,
private RequestStack $requestStack,
private NotifierInterface $notifier,
private LoggerInterface $logger,
) {}
public function alert(string $title, string $message, string $type = "success"): void
public function alert(string $title, string $message, string $type = "success", bool $sendPush = false): 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,
'type' => $type,
])
);
$this->hub->publish($update);
try {
$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,
'type' => $type,
])
);
$this->hub->publish($update);
} catch (\Throwable $exception) {
// ToDo: look for better handling to get message to end user
}
if (true === $sendPush && in_array($this->notificationTransport, ['ntfy'])) {
try {
$notification = new Notification($title, ['push'])->content($message);
$this->notifier->send($notification);
} catch (\Throwable $exception) {
$this->logger->error('Unable to send push notification: ' . $exception->getMessage());
}
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Download\Downloader;
use App\Base\Service\Broadcaster;
use App\Base\Service\MediaFiles;
use App\Download\Framework\Entity\Download;
use Doctrine\ORM\EntityManagerInterface;
@@ -19,6 +20,7 @@ class ProcessDownloader implements DownloaderInterface
private EntityManagerInterface $entityManager,
private MediaFiles $mediaFiles,
private CacheInterface $cache,
private readonly Broadcaster $broadcaster,
) {}
/**
@@ -82,6 +84,7 @@ class ProcessDownloader implements DownloaderInterface
});
if ($downloadEntity->getStatus() !== 'Paused') {
$downloadEntity->setProgress(100);
$this->alertComplete($downloadEntity);
}
} catch (ProcessFailedException $exception) {
$downloadEntity->setStatus('Failed');
@@ -105,4 +108,15 @@ class ProcessDownloader implements DownloaderInterface
throw new \Exception("There is no download path for media type: $mediaType");
}
private function alertComplete(Download $download): void
{
if ("tvshows" === $download->getMediaType()) {
$message = '"' . $download->getTitle() . '" - ' . $download->getEpisodeId() . ' has finished downloading.';
} else {
$message = '"' . $download->getTitle() . '" has finished downloading.';
}
$this->broadcaster->alert('Success', $message, sendPush: true);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\User\Database;
class CodecList
{
public static $codecs = [
'-',
'h264',
'h265/HEVC',
];
@@ -16,9 +17,13 @@ class CodecList
public static function asSelectOptions(): array
{
return [
'h264' => 'h264',
'h265/HEVC' => 'h265',
];
$result = [];
foreach (static::$codecs as $codec) {
$result[$codec] = $codec;
}
$result['h265/HEVC'] = 'h265';
return $result;
}
}

View File

@@ -26,7 +26,7 @@ class ProviderList
public static function asSelectOptions(): array
{
$result = [];
$result = ['-' => '-'];
foreach (static::$providers as $provider) {
$result[$provider] = $provider;
}

View File

@@ -102,7 +102,7 @@ class QualityList
public static function asSelectOptions(): array
{
$result = ['n/a' => null];
$result = ['n/a' => null, '-' => '-'];
foreach (array_keys(static::$qualities) as $quality) {
$result[$quality] = $quality;
}

View File

@@ -18,7 +18,7 @@ class ResolutionList
public static function asSelectOptions(): array
{
$result = [];
$result = ['-' => '-'];
foreach (static::$resolutions as $resolution) {
$result[$resolution] = $resolution;
}

View File

@@ -36,6 +36,8 @@ class PreferencesController extends AbstractController
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
// dd($form);
return $this->render(
'user/preferences.html.twig',
[

View File

@@ -232,6 +232,27 @@
"config/packages/messenger.yaml"
]
},
"symfony/notifier": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.0",
"ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
},
"files": [
"config/packages/notifier.yaml"
]
},
"symfony/ntfy-notifier": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "0d5e496659d7361bb4e6648eb8332f8cf097533d"
}
},
"symfony/property-info": {
"version": "7.3",
"recipe": {

View File

@@ -30,5 +30,6 @@
{% block body %}{% endblock %}
</div>
</div>
<twig:PreviewModal id="previewModal" />
</body>
</html>

View File

@@ -14,10 +14,15 @@
{% if entity.status != "Complete" %}
<turbo-stream action="update" target="download_progress_{{ id }}">
<template>
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
{% if entity.progress >= 50 %}
{% set text_color = "text-black dark:text-black" %}
{% else %}
{% set text_color = "text-black dark:text-white" %}
{% endif %}
<div class="background {{ text_color }} text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
style="width: {{ entity.progress }}%">
</div>
<div class="number text-black font-bold text-center z-40"
<div class="number {{ text_color }} font-bold text-center z-40"
>{{ entity.progress }}%</div>
</template>
</turbo-stream>
@@ -50,7 +55,11 @@
<turbo-stream action="prepend" target="alert_list">
<template>
<twig:Alert title="Finished downloading" message="{{ entity.title }}" alert_id="{{ entity.id }}" data-controller="alert" />
{% if entity.mediaType == "tvshows" %}
<twig:Alert title="Success" message="{{ entity.title }} - ({{ entity.episodeId }}) has finished downloading." alert_id="{{ entity.id }}" data-controller="alert" />
{% else %}
<twig:Alert title="Success" message="{{ entity.title }} has finished downloading." alert_id="{{ entity.id }}" data-controller="alert" />
{% endif %}
</template>
</turbo-stream>

View File

@@ -6,8 +6,12 @@
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">Info</span>
<h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3>
<twig:ux:icon name="ic:twotone-cancel" style="text-align:right" width="16.75px" height="16.75px" class="modal-close rounded-full align-end text-red-600 hover:text-red-700" />
<span class="sr-only">Info</span>
</div>
<div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap">
{{ message }}

View File

@@ -1,13 +1,11 @@
<div{{ attributes }}>
<div class="flex flex-col bg-sky-950 border-neutral-700 border-t-4 border-t-orange-500 rounded-xl
backdrop-filter backdrop-blur-md bg-opacity-40 z-10
">
<div class="flex flex-col bg-sky-950/40 border-neutral-700 border-t-4 border-t-orange-500 rounded-xl">
<div class="p-4 md:p-5">
<h3 class="mb-4 text-lg font-bold text-white">
{{ title }}
</h3>
<div class="{{ contentClass|default('flex flex-col overflow-hidden rounded-md') }}">
<div class="{{ contentClass|default('flex flex-col rounded-md') }}">
{% block content %}{% endblock %}
</div>
</div>

View File

@@ -7,45 +7,45 @@
</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') }}>
<table id="downloads" class="divide-y divide-gray-200 dark:divide-gray-800 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">
<tr class="bg-orange-500/80 text-gray-800 dark:text-stone-800 text-xs font-medium uppercase">
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
class="px-6 py-3 truncate text-start">
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
class="px-6 py-3 truncate text-start {{ isWidget == true ? "hidden" : "r-tablecell" }}">
Filename
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
class="px-6 py-3 truncate text-start {{ isWidget == true ? "hidden" : "r-tablecell" }}">
Media type
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
class="px-6 py-3 text-start">
Progress
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
class="px-6 py-3 text-start">
</th>
</tr>
</thead>
<tbody id="{{ table_body_id }}" class="divide-y divide-gray-200 dark:divide-gray-50" data-download-list-target="download">
<tbody id="{{ table_body_id }}" class="dark:text-white divide-y divide-gray-200 dark:divide-gray-900" data-download-list-target="download">
{% if this.downloads.items|length > 0 %}
{% for download in this.downloads.items %}
<twig:DownloadListRow download="{{ download }}" isWidget="{{ 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-300 uppercase text-xs font-medium text-center text-black truncate" colspan="100%">
<td class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black" colspan="100%">
<a href="{{ path('app_downloads') }}">View All Downloads</a>
</td>
</tr>
{% endif %}
{% else %}
<tr id="{{ table_body_id }}_no_downloads">
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center font-medium text-gray-800 dark:text-stone-800" colspan="100%">
<tr id="{{ table_body_id }}_no_downloads" class="text-black dark:text-white dark:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center font-medium" colspan="100%">
No downloads
</td>
</tr>

View File

@@ -1,8 +1,18 @@
<tr{{ attributes }} is="download-list-row" class="hover:bg-gray-200" id="ad_download_{{ download.id }}" data-title="{{ download.title }}"
download-id="{{ download.id }}" imdb-id="{{ download.imdbId }}" media-title="{{ download.title }}" url="{{ download.url }}" filename="{{ download.filename }}" status="{{ download.status }}" progress="{{ download.progress }}" media-type="{{ download.mediaType }}" episode-id="{{ download.episodeId }}" created-at="{{ download.createdAt|date('m/d/Y g:i a') }}" updated-at="{{ download.updatedAt|date }}"
<tr{{ attributes }} is="download-list-row" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900" id="ad_download_{{ download.id }}" data-title="{{ download.title }}"
download-id="{{ download.id }}"
imdb-id="{{ download.imdbId }}"
media-title="{{ download.title }}"
url="{{ download.url }}"
filename="{{ download.filename }}"
status="{{ download.status }}"
progress="{{ download.progress }}"
media-type="{{ download.mediaType }}"
episode-id="{{ download.episodeId }}"
created-at="{{ download.createdAt|date('m/d/Y g:i a') }}"
updated-at="{{ download.updatedAt|date('m/d/Y g:i a') }}"
data-filename="{{ download.filename }}" data-media-type="{{ download.mediaType }}" data-status="{{ download.status }}" data-progress="{{ download.progress }}"
>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium truncate">
{% if download.mediaType == "movies" %}
{% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType} %}
{% set route = path('app_search_result', routeParams) %}
@@ -21,11 +31,11 @@
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 max-w-[60ch] {{ isWidget == true ? "hidden" : "r-tablecell" }} truncate">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium max-w-[60ch] {{ isWidget == true ? "hidden" : "r-tablecell" }} truncate">
{{ download.filename }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium truncate {{ isWidget == true ? "hidden" : "r-tablecell" }}">
{{ download.mediaType }}
</td>
@@ -36,7 +46,7 @@
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
style="width: {{ download.progress }}%">
</div>
<div class="number text-black font-bold text-center z-40"
<div class="number text-black dark:text-white font-bold text-center z-40"
>{{ download.progress }}%</div>
</div>
</div>

View File

@@ -4,55 +4,57 @@
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
</div>
{% endif %}
<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') }}>
<table id="monitor_list" class="divide-y divide-gray-200 dark:divide-gray-800 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Monitor\\Framework\\Entity\\Monitor') }}
{{ stimulus_target('monitor_list', 'monitorList') }}
>
<thead>
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
<tr class="bg-orange-500/80 text-gray-800 dark:text-stone-800 text-xs font-medium uppercase">
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase truncate">
class="px-6 py-3 text-start truncate">
Title
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start">
ID
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="hidden md:table-cell px-6 py-3 text-start">
Search Count
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="hidden md:table-cell px-6 py-3 text-start">
Created at
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="hidden md:table-cell px-6 py-3 text-start">
Last Search Date
</th>
<th scope="col"
class="hidden md:table-cell px-6 py-3 text-start text-xs font-medium uppercase">
class="hidden md:table-cell px-6 py-3 text-start">
Type
</th>
<th scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase">
class="px-6 py-3 text-start">
Status
</th>
<th class="hidden md:table-cell"></th>
</tr>
</thead>
<tbody id="monitors" class="divide-y divide-gray-50">
<tbody id="monitors" class="dark:text-white divide-y divide-gray-200 dark:divide-gray-900">
{% if this.monitors.items|length > 0 %}
{% for monitor in this.monitors.items %}
<twig:MonitorListRow :monitor="monitor" isWidget="{{ this.isWidget }}" />
{% endfor %}
{% if this.isWidget and this.monitors.items|length > 5 %}
<tr id="monitor_view_all">
<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">
<td colspan="100%" class="py-2 whitespace-nowrap bg-gray-400 dark:bg-gray-700 uppercase text-xs font-medium text-center text-black dark:text-white min-w-[50ch] max-w-[50ch] truncate">
<a href="{{ path('app_monitors') }}">View All Monitors</a>
</td>
</tr>
{% endif %}
{% else %}
<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%">
<td class="px-6 py-4 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 text-center text-xs uppercase font-medium" colspan="100%">
No monitors
</td>
</tr>

View File

@@ -1,4 +1,17 @@
<tr{{ attributes }} id="monitor_{{ monitor.id }}" class="hover:bg-gray-200">
<tr{{ attributes }} is="monitor-list-row" id="monitor_{{ monitor.id }}" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900"
monitor-id="{{ monitor.id }}"
imdb-id="{{ monitor.imdbId }}"
media-title="{{ monitor.title }}"
season="{{ monitor.season }}"
episode="{{ monitor.episode }}"
status="{{ monitor.status }}"
search-count="{{ monitor.searchCount }}"
media-type="{{ monitor.monitorType|monitor_type }}"
episode-id="{{ monitor|monitor_media_id }}"
created-at="{{ monitor.createdAt|date('m/d/Y g:i a') }}"
last-search="{{ monitor.lastSearch|date('m/d/Y g:i a') }}"
downloaded-at="{{ monitor.downloadedAt|date('m/d/Y g:i a') }}"
>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 truncate">
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
class="mr-1 hover:underline rounded-md"
@@ -13,24 +26,24 @@
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
{% endif %}
<a href="{{ route }}"
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate">
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate dark:text-white">
{{ monitor.title }}
</a>
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm">
{{ monitor|monitor_media_id }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
{{ monitor.searchCount }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
{{ monitor.createdAt|date('m/d/Y h:i a') }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
</td>
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
{% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %}
@@ -39,7 +52,7 @@
<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">
<td class="px-6 py-4 whitespace-nowrap text-sm">
{% if monitor.status == "New" %}
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
@@ -59,11 +72,11 @@
{{ monitor.title }}
</td>
{% if monitor|monitor_media_id != "-" %}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm">
{{ monitor|monitor_media_id }}
</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm">
{% if monitor.monitorType == "tvshow" %}
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
{% elseif monitor.monitorType == "tvseason" %}
@@ -72,7 +85,7 @@
<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">
<td class="px-6 py-4 whitespace-nowrap text-sm">
{% if monitor.status == "New" %}
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}

View File

@@ -1,8 +1,8 @@
<dialog{{ attributes }} is="preview-content-dialog" class="py-3 px-4 w-full md:w-[50rem] rounded-md dark:bg-gray-950/80 dark:border dark:border-orange-500 dark:text-white backdrop-filter backdrop-blur-md">
<dialog{{ attributes }} is="preview-content-dialog" class="py-3 px-4 w-full md:w-[50rem] rounded-md dark:bg-gray-950/80 dark:border-2 dark:border-orange-500 dark:text-white backdrop-filter backdrop-blur-3xl">
<div class="flex flex-row justify-end">
<twig:ux:icon name="ic:twotone-cancel" width="16.75px" height="16.75px" class="modal-close rounded-full align-middle text-red-600 hover:text-red-700" />
</div>
<h2 class="modal-heading mb-4 text-2xl font-bold">{{ heading|default('') }}</h2>
<h2 class="modal-heading mb-4 text-2xl font-bold text-orange-500">{{ heading|default('') }}</h2>
<div class="modal-content mb-4">
{% block content %}{% endblock %}

View File

@@ -15,6 +15,4 @@
<twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList>
</twig:Card>
</div>
<twig:PreviewModal id="previewModal" />
{% endblock %}