Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b587302b30 | |||
| e5bab8e6fd | |||
| 502b85dda4 | |||
| 9c430290e9 | |||
| 583591bf4f | |||
| 182708b8f0 | |||
| d6ba4d7d2a | |||
| e5c5ec93a8 | |||
| 942911d8ef | |||
| 2f7d406d12 | |||
| 4e1adc576c | |||
| 575fc08f24 | |||
| 87bdde801d | |||
| 7d35b6266b | |||
| caeda625fd | |||
| d710e31d2b | |||
| 39a64faa74 | |||
| c6a84df2fd | |||
| a7273cf2e5 | |||
| c9cfa5e427 | |||
| cb50007208 |
7
.env
7
.env
@@ -51,3 +51,10 @@ OIDC_CLIENT_ID="Enter your OIDC client id"
|
|||||||
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
|
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
|
||||||
OIDC_BYPASS_FORM_LOGIN=false
|
OIDC_BYPASS_FORM_LOGIN=false
|
||||||
###< drenso/symfony-oidc-bundle ###
|
###< drenso/symfony-oidc-bundle ###
|
||||||
|
|
||||||
|
###> symfony/ntfy-notifier ###
|
||||||
|
# NTFY_DSN=ntfy://default/TOPIC
|
||||||
|
###< symfony/ntfy-notifier ###
|
||||||
|
|
||||||
|
NOTIFICATION_TRANSPORT=
|
||||||
|
NTFY_DSN=
|
||||||
|
|||||||
@@ -18,11 +18,3 @@ var observer = new MutationObserver(function(mutations) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
||||||
|
|
||||||
const ptr = PullToRefresh.init({
|
|
||||||
mainElement: 'body',
|
|
||||||
onRefresh() {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|||||||
2
assets/bootstrap.js
vendored
2
assets/bootstrap.js
vendored
@@ -3,6 +3,7 @@ import PreviewContentDialog from "./components/preview-content-dialog.js";
|
|||||||
import EpisodeContainer from './components/episode-container.js';
|
import EpisodeContainer from './components/episode-container.js';
|
||||||
import DownloadOptionTr from './components/download-option-tr.js';
|
import DownloadOptionTr from './components/download-option-tr.js';
|
||||||
import DownloadListRow from './components/download-list-row.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 MovieContainer from "./components/movie-container.js";
|
||||||
|
|
||||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||||
@@ -22,3 +23,4 @@ customElements.define('episode-container', EpisodeContainer);
|
|||||||
customElements.define('movie-container', MovieContainer);
|
customElements.define('movie-container', MovieContainer);
|
||||||
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
|
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
|
||||||
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
|
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
|
||||||
|
customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'});
|
||||||
|
|||||||
115
assets/components/monitor-list-row.js
Normal file
115
assets/components/monitor-list-row.js
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,11 @@ export default class PreviewContentDialog extends HTMLDialogElement {
|
|||||||
this.setHeading = this.setHeading.bind(this);
|
this.setHeading = this.setHeading.bind(this);
|
||||||
this.setContent = this.setContent.bind(this);
|
this.setContent = this.setContent.bind(this);
|
||||||
|
|
||||||
|
this.#closeBtnEl.addEventListener('click', () => this.close());
|
||||||
|
document.addEventListener('hidePreviewContentModal', () => this.close());
|
||||||
document.addEventListener('showPreviewContentModal', (event) => {
|
document.addEventListener('showPreviewContentModal', (event) => {
|
||||||
this.display(event.detail);
|
this.display(event.detail);
|
||||||
});
|
});
|
||||||
document.addEventListener('hidePreviewContentModal', (e) => this.close());
|
|
||||||
this.#closeBtnEl.addEventListener('click', () => this.close());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeading(heading) {
|
setHeading(heading) {
|
||||||
|
|||||||
@@ -41,23 +41,20 @@ export default class extends Controller {
|
|||||||
downloadTargetConnected(target) {
|
downloadTargetConnected(target) {
|
||||||
let downloads = this.element.querySelectorAll('tbody tr');
|
let downloads = this.element.querySelectorAll('tbody tr');
|
||||||
|
|
||||||
console.log(target)
|
|
||||||
|
|
||||||
downloads.forEach(download => {
|
downloads.forEach(download => {
|
||||||
download.addEventListener('click', (event) => {
|
download.addEventListener('click', (event) => {
|
||||||
// let previewContentModal = document.querySelector('#previewContentModal');
|
|
||||||
let content, heading = ""
|
let content, heading = ""
|
||||||
if (event.target.tagName !== "TR") {
|
if (event.target.tagName !== "TR") {
|
||||||
content = event.target.parentElement.previewContent;
|
content = event.target.parentElement.previewContent;
|
||||||
heading = event.target.parentElement.mediaTitle;
|
heading = "Download # " + event.target.parentElement.downloadId + " - \"" + event.target.parentElement.mediaTitle + "\"";
|
||||||
} else {
|
} else {
|
||||||
content = event.target.previewContent;
|
content = event.target.previewContent;
|
||||||
heading = event.target.mediaTitle;
|
heading = "Download # " + event.target.downloadId + " - \"" + event.target.mediaTitle + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(content)
|
if (null !== content && undefined !== content && "" !== content) {
|
||||||
|
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
|
||||||
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,29 @@ import { Controller } from '@hotwired/stimulus';
|
|||||||
*/
|
*/
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
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) {
|
deleteMonitor(data) {
|
||||||
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
|
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
|||||||
57
assets/controllers/upcoming_episodes_controller.js
Normal file
57
assets/controllers/upcoming_episodes_controller.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
import { Calendar } from '@fullcalendar/core';
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||||
|
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
calendar = null;
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.calendar = new Calendar(this.element, {
|
||||||
|
plugins: [ dayGridPlugin, timeGridPlugin ],
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
editable: true, // Allow events to be dragged and resized
|
||||||
|
events: '/api/events', // Symfony route to fetch events
|
||||||
|
eventDrop: function(info) {
|
||||||
|
// Handle event drop (e.g., update event in database via AJAX)
|
||||||
|
},
|
||||||
|
eventResize: function(info) {
|
||||||
|
// Handle event resize (e.g., update event in database via AJAX)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.calendar.render();
|
||||||
|
// this.calendar = new Calendar(this.element, {
|
||||||
|
// plugins: [ dayGridPlugin, timeGridPlugin, listPlugin ],
|
||||||
|
// initialView: 'dayGridMonth',
|
||||||
|
// headerToolbar: {
|
||||||
|
// left: 'prev,next today',
|
||||||
|
// center: 'title',
|
||||||
|
// right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// this.calendar.render();
|
||||||
|
// calendar.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom controller actions here
|
||||||
|
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Called anytime its element is disconnected from the DOM
|
||||||
|
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||||
|
|
||||||
|
// Here you should remove all event listeners added in "connect()"
|
||||||
|
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 529 B |
1
assets/icons/solar/calendar-linear.svg
Normal file
1
assets/icons/solar/calendar-linear.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="currentColor" stroke-width="1.5" d="M2 12c0-3.771 0-5.657 1.172-6.828S6.229 4 10 4h4c3.771 0 5.657 0 6.828 1.172S22 8.229 22 12v2c0 3.771 0 5.657-1.172 6.828S17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.172S2 17.771 2 14z"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M7 4V2.5M17 4V2.5M2.5 9h19"/><path fill="currentColor" d="M18 17a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 653 B |
@@ -27,6 +27,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--fc-border-color: #a65b27;
|
||||||
|
--fc-page-bg-color: #a65b27;
|
||||||
|
}
|
||||||
|
|
||||||
/* Prevent scrolling while dialog is open */
|
/* Prevent scrolling while dialog is open */
|
||||||
body:has(dialog[data-dialog-target="dialog"][open]) {
|
body:has(dialog[data-dialog-target="dialog"][open]) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -55,6 +60,10 @@ dialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog[open] {
|
||||||
|
animation: fade-in 100ms ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add animations */
|
/* Add animations */
|
||||||
dialog[data-dialog-target="dialog"][open] {
|
dialog[data-dialog-target="dialog"][open] {
|
||||||
animation: fade-in 200ms forwards;
|
animation: fade-in 200ms forwards;
|
||||||
@@ -133,7 +142,7 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
|
|
||||||
#search .ts-dropdown {
|
#search .ts-dropdown {
|
||||||
background: unset;
|
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 {
|
#search .ts-dropdown .ts-dropdown-content .option.active {
|
||||||
@@ -189,3 +198,7 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
.filter-label {
|
.filter-label {
|
||||||
@apply flex flex-col gap-1 justify-between;
|
@apply flex flex-col gap-1 justify-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fc-col-header-cell {
|
||||||
|
@apply bg-orange-500/60 text-white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"php-tmdb/api": "^4.1",
|
"php-tmdb/api": "^4.1",
|
||||||
"predis/predis": "^2.4",
|
"predis/predis": "^2.4",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
|
"spatie/icalendar-generator": "^3.0",
|
||||||
"spomky-labs/pwa-bundle": "^1.2",
|
"spomky-labs/pwa-bundle": "^1.2",
|
||||||
"stof/doctrine-extensions-bundle": "^1.14",
|
"stof/doctrine-extensions-bundle": "^1.14",
|
||||||
"symfony/asset": "7.3.*",
|
"symfony/asset": "7.3.*",
|
||||||
@@ -43,6 +44,8 @@
|
|||||||
"symfony/mailer": "7.3.*",
|
"symfony/mailer": "7.3.*",
|
||||||
"symfony/mercure-bundle": "^0.3.9",
|
"symfony/mercure-bundle": "^0.3.9",
|
||||||
"symfony/messenger": "7.3.*",
|
"symfony/messenger": "7.3.*",
|
||||||
|
"symfony/notifier": "7.3.*",
|
||||||
|
"symfony/ntfy-notifier": "7.3.*",
|
||||||
"symfony/runtime": "7.3.*",
|
"symfony/runtime": "7.3.*",
|
||||||
"symfony/scheduler": "7.3.*",
|
"symfony/scheduler": "7.3.*",
|
||||||
"symfony/security-bundle": "7.3.*",
|
"symfony/security-bundle": "7.3.*",
|
||||||
|
|||||||
207
composer.lock
generated
207
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f4953f7b233a466d94269f35096f3385",
|
"content-hash": "9ffd10f98137e8975de2c04ac2412ed5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -4757,6 +4757,65 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-12-12T12:06:11+00:00"
|
"time": "2023-12-12T12:06:11+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/icalendar-generator",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/icalendar-generator.git",
|
||||||
|
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||||
|
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"larapack/dd": "^1.1",
|
||||||
|
"nesbot/carbon": "^3.5",
|
||||||
|
"pestphp/pest": "^2.34",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"spatie/pest-plugin-snapshots": "^2.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\IcalendarGenerator\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ruben Van Assche",
|
||||||
|
"email": "ruben@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Build calendars in the iCalendar format",
|
||||||
|
"homepage": "https://github.com/spatie/icalendar-generator",
|
||||||
|
"keywords": [
|
||||||
|
"calendar",
|
||||||
|
"iCalendar",
|
||||||
|
"ical",
|
||||||
|
"ics",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/icalendar-generator/issues",
|
||||||
|
"source": "https://github.com/spatie/icalendar-generator/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2025-04-17T14:50:03+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spomky-labs/pki-framework",
|
"name": "spomky-labs/pki-framework",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -7578,6 +7637,152 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-02-19T08:51:26+00:00"
|
"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",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
|
|||||||
13
config/packages/notifier.yaml
Normal file
13
config/packages/notifier.yaml
Normal 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 }
|
||||||
@@ -44,6 +44,10 @@ parameters:
|
|||||||
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
|
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
|
||||||
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
|
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
||||||
|
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
tty: true
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 2
|
replicas: 2
|
||||||
volumes:
|
volumes:
|
||||||
@@ -16,6 +15,10 @@ services:
|
|||||||
- mercure_config:/config
|
- mercure_config:/config
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
logging:
|
||||||
|
driver: "gelf"
|
||||||
|
options:
|
||||||
|
gelf-address: "tcp://192.168.1.197:12202"
|
||||||
|
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
@@ -29,6 +32,10 @@ services:
|
|||||||
replicas: 2
|
replicas: 2
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
|
logging:
|
||||||
|
driver: "gelf"
|
||||||
|
options:
|
||||||
|
gelf-address: "tcp://192.168.1.197:12203"
|
||||||
|
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
@@ -40,6 +47,11 @@ services:
|
|||||||
command: -vv
|
command: -vv
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
|
logging:
|
||||||
|
driver: "gelf"
|
||||||
|
options:
|
||||||
|
gelf-address: "tcp://192.168.1.197:12204"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
35
migrations/Version20250823173128.php
Normal file
35
migrations/Version20250823173128.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250823173128 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor ADD air_date DATETIME DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor DROP air_date
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,12 @@ final class ConfigResolver
|
|||||||
|
|
||||||
#[Autowire(param: 'auth.oidc.bypass_form_login')]
|
#[Autowire(param: 'auth.oidc.bypass_form_login')]
|
||||||
private ?bool $authOidcBypassFormLogin = null,
|
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
|
public function validate(): bool
|
||||||
@@ -54,6 +60,12 @@ final class ConfigResolver
|
|||||||
$valid = false;
|
$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;
|
return $valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ final class IndexController extends AbstractController
|
|||||||
#[Route('/test')]
|
#[Route('/test')]
|
||||||
public function monitorTvShow(): Response
|
public function monitorTvShow(): Response
|
||||||
{
|
{
|
||||||
$this->monitorTvShowHandler->handle(new MonitorTvShowCommand(96));
|
return $this->render('index/test.html.twig', []);
|
||||||
return $this->json([
|
|
||||||
'Success' => 'Monitor added'
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,53 @@
|
|||||||
|
|
||||||
namespace App\Base\Service;
|
namespace App\Base\Service;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\Mercure\HubInterface;
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
use Symfony\Component\Mercure\Update;
|
use Symfony\Component\Mercure\Update;
|
||||||
|
use Symfony\Component\Notifier\Notification\Notification;
|
||||||
|
use Symfony\Component\Notifier\NotifierInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
readonly class Broadcaster
|
readonly class Broadcaster
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[Autowire(param: 'notification.transport')]
|
||||||
|
private string $notificationTransport,
|
||||||
#[Autowire(service: 'twig')]
|
#[Autowire(service: 'twig')]
|
||||||
private Environment $renderer,
|
private Environment $renderer,
|
||||||
private HubInterface $hub,
|
private HubInterface $hub,
|
||||||
private RequestStack $requestStack,
|
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');
|
try {
|
||||||
$update = new Update(
|
$userAlertTopic = $this->requestStack->getCurrentRequest()->getSession()->get('mercure_alert_topic');
|
||||||
$userAlertTopic,
|
$update = new Update(
|
||||||
$this->renderer->render('broadcast/Alert.stream.html.twig', [
|
$userAlertTopic,
|
||||||
'alert_id' => uniqid(),
|
$this->renderer->render('broadcast/Alert.stream.html.twig', [
|
||||||
'title' => $title,
|
'alert_id' => uniqid(),
|
||||||
'message' => $message,
|
'title' => $title,
|
||||||
'type' => $type,
|
'message' => $message,
|
||||||
])
|
'type' => $type,
|
||||||
);
|
])
|
||||||
$this->hub->publish($update);
|
);
|
||||||
|
$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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Download\Downloader;
|
namespace App\Download\Downloader;
|
||||||
|
|
||||||
|
use App\Base\Service\Broadcaster;
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -19,6 +20,7 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private CacheInterface $cache,
|
private CacheInterface $cache,
|
||||||
|
private readonly Broadcaster $broadcaster,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +84,7 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
});
|
});
|
||||||
if ($downloadEntity->getStatus() !== 'Paused') {
|
if ($downloadEntity->getStatus() !== 'Paused') {
|
||||||
$downloadEntity->setProgress(100);
|
$downloadEntity->setProgress(100);
|
||||||
|
$this->alertComplete($downloadEntity);
|
||||||
}
|
}
|
||||||
} catch (ProcessFailedException $exception) {
|
} catch (ProcessFailedException $exception) {
|
||||||
$downloadEntity->setStatus('Failed');
|
$downloadEntity->setStatus('Failed');
|
||||||
@@ -105,4 +108,15 @@ class ProcessDownloader implements DownloaderInterface
|
|||||||
|
|
||||||
throw new \Exception("There is no download path for media type: $mediaType");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ namespace App\Monitor\Action\Handler;
|
|||||||
use App\Base\Util\EpisodeId;
|
use App\Base\Util\EpisodeId;
|
||||||
use App\Download\Action\Command\DownloadMediaCommand;
|
use App\Download\Action\Command\DownloadMediaCommand;
|
||||||
use App\Download\DownloadOptionEvaluator;
|
use App\Download\DownloadOptionEvaluator;
|
||||||
use App\Download\Framework\Entity\Download;
|
|
||||||
use App\Download\Framework\Repository\DownloadRepository;
|
use App\Download\Framework\Repository\DownloadRepository;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
@@ -44,6 +43,11 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
||||||
|
|
||||||
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||||
|
|
||||||
|
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
||||||
|
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
||||||
|
}
|
||||||
|
|
||||||
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
||||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
|
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
|
||||||
return new MonitorTvEpisodeResult(
|
return new MonitorTvEpisodeResult(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Monitor\Framework\Entity\Monitor;
|
|||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use App\Base\Util\PTN;
|
use App\Base\Util\PTN;
|
||||||
@@ -96,6 +97,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
->setMonitorType('tvepisode')
|
->setMonitorType('tvepisode')
|
||||||
->setSeason($episode['season_number'])
|
->setSeason($episode['season_number'])
|
||||||
->setEpisode($episode['episode_number'])
|
->setEpisode($episode['episode_number'])
|
||||||
|
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
|
||||||
->setCreatedAt(new DateTimeImmutable())
|
->setCreatedAt(new DateTimeImmutable())
|
||||||
->setSearchCount(0)
|
->setSearchCount(0)
|
||||||
->setStatus('New');
|
->setStatus('New');
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Monitor\Framework\Controller;
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||||
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
use App\Monitor\Action\Handler\DeleteMonitorHandler;
|
||||||
use App\Monitor\Action\Input\AddMonitorInput;
|
use App\Monitor\Action\Input\AddMonitorInput;
|
||||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
|
use App\Monitor\Framework\Scheduler\MonitorDispatcher;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -65,4 +67,45 @@ class ApiController extends AbstractController
|
|||||||
'message' => 'Manually dispatched MonitorDispatcher'
|
'message' => 'Manually dispatched MonitorDispatcher'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/monitor/upcoming-episodes', name: 'api.monitor.upcoming-episodes', methods: ['GET'])]
|
||||||
|
public function upcomingEpisodes(MonitorRepository $repository): Response
|
||||||
|
{
|
||||||
|
$colors = [
|
||||||
|
'blue' => '#007bff',
|
||||||
|
'indigo' => '#6610f2',
|
||||||
|
'purple' => '#6f42c1',
|
||||||
|
'pink' => '#e83e8c',
|
||||||
|
'red' => '#dc3545',
|
||||||
|
'orange' => '#fd7e14',
|
||||||
|
'yellow' => '#ffc107',
|
||||||
|
'green' => '#28a745',
|
||||||
|
'teal' => '#20c997',
|
||||||
|
'cyan' => '#17a2b8',
|
||||||
|
];
|
||||||
|
|
||||||
|
$eventColors = [];
|
||||||
|
$monitors = $repository->whereAirDateNotNull();
|
||||||
|
$monitors = Map::from($monitors)->map(function ($monitor) use (&$eventColors, $colors) {
|
||||||
|
if (!array_key_exists($monitor->getImdbId(), $eventColors)) {
|
||||||
|
$eventColors[$monitor->getImdbId()] = $colors[array_rand($colors)];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $monitor->getId(),
|
||||||
|
'title' => $monitor->getTitle() . ' (S' . str_pad($monitor->getSeason(), 2, '0', STR_PAD_LEFT) . 'E' . str_pad($monitor->getEpisode(), 2, '0', STR_PAD_LEFT) . ')',
|
||||||
|
'start' => $monitor->getAirDate()->format('Y-m-d H:i:s'),
|
||||||
|
'groupId' => $monitor->getImdbId(),
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
||||||
|
'borderColor' => $eventColors[$monitor->getImdbId()],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'status' => 200,
|
||||||
|
'data' => [
|
||||||
|
'episodes' => $monitors->toArray(),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/Monitor/Framework/Controller/CalendarController.php
Normal file
33
src/Monitor/Framework/Controller/CalendarController.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||||
|
use Spatie\IcalendarGenerator\Components\Event;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class CalendarController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/monitors/ical.ics', name: 'app.monitors.ical')]
|
||||||
|
public function icalAction(MonitorRepository $monitorRepository)
|
||||||
|
{
|
||||||
|
$calendar = new Calendar();
|
||||||
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
|
$events = Map::from($monitors)->map(function ($monitor) {
|
||||||
|
return new Event($monitor->getTitle())
|
||||||
|
->startsAt($monitor->getAirDate())
|
||||||
|
->withoutTimezone()
|
||||||
|
->fullDay()
|
||||||
|
;
|
||||||
|
});
|
||||||
|
$calendar->event($events->toArray());
|
||||||
|
return new Response($calendar->get(), 200, [
|
||||||
|
'Content-Type' => 'text/calendar',
|
||||||
|
'Content-Disposition' => 'attachment; filename="upcoming-episodes.ics"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,4 +28,10 @@ class WebController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('monitor/index.html.twig');
|
return $this->render('monitor/index.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/monitors/upcoming-episodes', name: 'app.monitor.upcoming-episodes', methods: ['GET'])]
|
||||||
|
public function upcomingEpisodes()
|
||||||
|
{
|
||||||
|
return $this->render('monitor/upcoming-episodes.html.twig');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class Monitor
|
|||||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
private ?\DateTimeInterface $lastSearch = null;
|
private ?\DateTimeInterface $lastSearch = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
private ?\DateTime $airDate = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?\DateTimeImmutable $createdAt = null;
|
private ?\DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
@@ -257,6 +260,17 @@ class Monitor
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAirDate(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->airDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAirDate(?\DateTimeInterface $airDate): static
|
||||||
|
{
|
||||||
|
$this->airDate = $airDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeChild(self $child): static
|
public function removeChild(self $child): static
|
||||||
{
|
{
|
||||||
if ($this->children->removeElement($child)) {
|
if ($this->children->removeElement($child)) {
|
||||||
|
|||||||
@@ -33,4 +33,12 @@ class MonitorRepository extends ServiceEntityRepository
|
|||||||
|
|
||||||
return $this->paginator->paginate($query, $page, $perPage);
|
return $this->paginator->paginate($query, $page, $perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function whereAirDateNotNull()
|
||||||
|
{
|
||||||
|
$query = $this->createQueryBuilder('m')
|
||||||
|
->andWhere('m.airDate IS NOT NULL')
|
||||||
|
->getQuery();
|
||||||
|
return $query->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,13 +286,18 @@ class Tmdb
|
|||||||
|
|
||||||
private function parseTvShow(array $data, string $posterBasePath): TmdbResult
|
private function parseTvShow(array $data, string $posterBasePath): TmdbResult
|
||||||
{
|
{
|
||||||
|
if (!in_array($data['first_air_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['first_air_date']))->format('Y-m-d');
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
return new TmdbResult(
|
return new TmdbResult(
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
imdbId: $data['external_ids']['imdb_id'],
|
||||||
tmdbId: $data['id'],
|
tmdbId: $data['id'],
|
||||||
title: $data['name'],
|
title: $data['name'],
|
||||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
||||||
description: $data['overview'],
|
description: $data['overview'],
|
||||||
year: (new \DateTime($data['first_air_date']))->format('Y'),
|
year: $airDate,
|
||||||
mediaType: "tvshows",
|
mediaType: "tvshows",
|
||||||
episodes: $data['episodes'],
|
episodes: $data['episodes'],
|
||||||
);
|
);
|
||||||
@@ -300,6 +305,11 @@ class Tmdb
|
|||||||
|
|
||||||
private function parseEpisode(array $data, string $posterBasePath): TmdbResult
|
private function parseEpisode(array $data, string $posterBasePath): TmdbResult
|
||||||
{
|
{
|
||||||
|
if (!in_array($data['air_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['air_date']))->format('Y-m-d');
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
return new TmdbResult(
|
return new TmdbResult(
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
imdbId: $data['external_ids']['imdb_id'],
|
||||||
tmdbId: $data['id'],
|
tmdbId: $data['id'],
|
||||||
@@ -309,12 +319,17 @@ class Tmdb
|
|||||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
year: (new \DateTime($data['air_date']))->format('Y'),
|
||||||
mediaType: "tvshows",
|
mediaType: "tvshows",
|
||||||
episodes: null,
|
episodes: null,
|
||||||
episodeAirDate: (new \DateTime($data['air_date']))->format('m/d/Y'),
|
episodeAirDate: $airDate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseMovie(array $data, string $posterBasePath): TmdbResult
|
private function parseMovie(array $data, string $posterBasePath): TmdbResult
|
||||||
{
|
{
|
||||||
|
if (!in_array($data['release_date'], ['', null,])) {
|
||||||
|
$airDate = (new \DateTime($data['release_date']))->format('Y-m-d');
|
||||||
|
} else {
|
||||||
|
$airDate = null;
|
||||||
|
}
|
||||||
return new TmdbResult(
|
return new TmdbResult(
|
||||||
imdbId: $data['external_ids']['imdb_id'],
|
imdbId: $data['external_ids']['imdb_id'],
|
||||||
tmdbId: $data['id'],
|
tmdbId: $data['id'],
|
||||||
@@ -323,7 +338,7 @@ class Tmdb
|
|||||||
description: $data['overview'],
|
description: $data['overview'],
|
||||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||||
mediaType: "movies",
|
mediaType: "movies",
|
||||||
episodeAirDate: (new \DateTime($data['release_date']))->format('m/d/Y'),
|
episodeAirDate: $airDate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\User\Database;
|
|||||||
class CodecList
|
class CodecList
|
||||||
{
|
{
|
||||||
public static $codecs = [
|
public static $codecs = [
|
||||||
|
'-',
|
||||||
'h264',
|
'h264',
|
||||||
'h265/HEVC',
|
'h265/HEVC',
|
||||||
];
|
];
|
||||||
@@ -16,9 +17,13 @@ class CodecList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
return [
|
$result = [];
|
||||||
'h264' => 'h264',
|
foreach (static::$codecs as $codec) {
|
||||||
'h265/HEVC' => 'h265',
|
$result[$codec] = $codec;
|
||||||
];
|
}
|
||||||
|
|
||||||
|
$result['h265/HEVC'] = 'h265';
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ProviderList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = ['-' => '-'];
|
||||||
foreach (static::$providers as $provider) {
|
foreach (static::$providers as $provider) {
|
||||||
$result[$provider] = $provider;
|
$result[$provider] = $provider;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class QualityList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
$result = ['n/a' => null];
|
$result = ['n/a' => null, '-' => '-'];
|
||||||
foreach (array_keys(static::$qualities) as $quality) {
|
foreach (array_keys(static::$qualities) as $quality) {
|
||||||
$result[$quality] = $quality;
|
$result[$quality] = $quality;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ResolutionList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = ['-' => '-'];
|
||||||
foreach (static::$resolutions as $resolution) {
|
foreach (static::$resolutions as $resolution) {
|
||||||
$result[$resolution] = $resolution;
|
$result[$resolution] = $resolution;
|
||||||
}
|
}
|
||||||
|
|||||||
21
symfony.lock
21
symfony.lock
@@ -232,6 +232,27 @@
|
|||||||
"config/packages/messenger.yaml"
|
"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": {
|
"symfony/property-info": {
|
||||||
"version": "7.3",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||||
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="flex flex-col bg-stone-700">
|
<body class="flex flex-col bg-stone-700">
|
||||||
@@ -30,5 +31,6 @@
|
|||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<twig:PreviewModal id="previewModal" />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,10 +14,15 @@
|
|||||||
{% if entity.status != "Complete" %}
|
{% if entity.status != "Complete" %}
|
||||||
<turbo-stream action="update" target="download_progress_{{ id }}">
|
<turbo-stream action="update" target="download_progress_{{ id }}">
|
||||||
<template>
|
<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 }}%">
|
style="width: {{ entity.progress }}%">
|
||||||
</div>
|
</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>
|
>{{ entity.progress }}%</div>
|
||||||
</template>
|
</template>
|
||||||
</turbo-stream>
|
</turbo-stream>
|
||||||
@@ -50,7 +55,11 @@
|
|||||||
|
|
||||||
<turbo-stream action="prepend" target="alert_list">
|
<turbo-stream action="prepend" target="alert_list">
|
||||||
<template>
|
<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>
|
</template>
|
||||||
</turbo-stream>
|
</turbo-stream>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span class="sr-only">Info</span>
|
|
||||||
<h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3>
|
<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>
|
||||||
<div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap">
|
<div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
<div{{ attributes }}>
|
<div{{ attributes }}>
|
||||||
<div class="flex flex-col bg-sky-950 border-neutral-700 border-t-4 border-t-orange-500 rounded-xl
|
<div class="flex flex-col bg-sky-950/40 border-neutral-700 border-t-4 border-t-orange-500 rounded-xl">
|
||||||
backdrop-filter backdrop-blur-md bg-opacity-40 z-10
|
|
||||||
">
|
|
||||||
<div class="p-4 md:p-5">
|
<div class="p-4 md:p-5">
|
||||||
<h3 class="mb-4 text-lg font-bold text-white">
|
<h3 class="mb-4 text-lg font-bold text-white">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="{{ contentClass|default('flex flex-col overflow-hidden rounded-md') }}">
|
<div class="{{ contentClass|default('flex flex-col rounded-md') }}">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,45 +7,45 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
<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"
|
<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
|
Title
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Filename
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Media type
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Progress
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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 %}
|
{% if this.downloads.items|length > 0 %}
|
||||||
{% for download in this.downloads.items %}
|
{% for download in this.downloads.items %}
|
||||||
<twig:DownloadListRow download="{{ download }}" isWidget="{{ isWidget }}" />
|
<twig:DownloadListRow download="{{ download }}" isWidget="{{ isWidget }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
|
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
|
||||||
<tr id="download_view_all">
|
<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>
|
<a href="{{ path('app_downloads') }}">View All Downloads</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr id="{{ table_body_id }}_no_downloads">
|
<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 text-gray-800 dark:text-stone-800" colspan="100%">
|
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center font-medium" colspan="100%">
|
||||||
No downloads
|
No downloads
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
<tr{{ attributes }} is="download-list-row" class="hover:bg-gray-200" id="ad_download_{{ download.id }}" data-title="{{ download.title }}"
|
<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 }}"
|
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 }}"
|
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" %}
|
{% if download.mediaType == "movies" %}
|
||||||
{% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType} %}
|
{% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType} %}
|
||||||
{% set route = path('app_search_result', routeParams) %}
|
{% set route = path('app_search_result', routeParams) %}
|
||||||
@@ -21,11 +31,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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 }}
|
{{ download.filename }}
|
||||||
</td>
|
</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 }}
|
{{ download.mediaType }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -36,7 +46,7 @@
|
|||||||
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
||||||
style="width: {{ download.progress }}%">
|
style="width: {{ download.progress }}%">
|
||||||
</div>
|
</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>
|
>{{ download.progress }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,16 @@
|
|||||||
<div class="md:flex md:items-center md:gap-12">
|
<div class="md:flex md:items-center md:gap-12">
|
||||||
<nav aria-label="Global" class="md:block">
|
<nav aria-label="Global" class="md:block">
|
||||||
<ul class="flex items-center gap-6 text-sm">
|
<ul class="flex items-center gap-6 text-sm">
|
||||||
|
<li>
|
||||||
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
||||||
|
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="hidden">
|
<li class="hidden">
|
||||||
<twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/>
|
<twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/>
|
||||||
</li>
|
</li>
|
||||||
<li class="hidden md:block">
|
<li class="hidden md:block">
|
||||||
<a href="{{ path('app_logout') }}">
|
<a href="{{ path('app_logout') }}" title="Log out of Torsearch.">
|
||||||
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
|
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -4,55 +4,57 @@
|
|||||||
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
|
<twig:DownloadSearch search_path="app_search" placeholder="Find {{ type == "complete" ? "a" : "an" }} {{ type }} monitor..." />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
<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"
|
<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
|
Title
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
class="px-6 py-3 text-start">
|
||||||
ID
|
ID
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Search Count
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Created at
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Last Search Date
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<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
|
Type
|
||||||
</th>
|
</th>
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
class="px-6 py-3 text-start">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th class="hidden md:table-cell"></th>
|
<th class="hidden md:table-cell"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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 %}
|
{% if this.monitors.items|length > 0 %}
|
||||||
{% for monitor in this.monitors.items %}
|
{% for monitor in this.monitors.items %}
|
||||||
<twig:MonitorListRow :monitor="monitor" isWidget="{{ this.isWidget }}" />
|
<twig:MonitorListRow :monitor="monitor" isWidget="{{ this.isWidget }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||||
<tr id="monitor_view_all">
|
<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>
|
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr id="active_monitors_no_monitors">
|
<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
|
No monitors
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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">
|
<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}) }}"
|
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
||||||
class="mr-1 hover:underline rounded-md"
|
class="mr-1 hover:underline rounded-md"
|
||||||
@@ -13,24 +26,24 @@
|
|||||||
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
|
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ route }}"
|
<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 }}
|
{{ monitor.title }}
|
||||||
</a>
|
</a>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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 }}
|
{{ monitor|monitor_media_id }}
|
||||||
</td>
|
</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 }}
|
{{ monitor.searchCount }}
|
||||||
</td>
|
</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') }}
|
{{ monitor.createdAt|date('m/d/Y h:i a') }}
|
||||||
</td>
|
</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') }}
|
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
|
||||||
</td>
|
</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" %}
|
{% if monitor.monitorType == "tvshow" %}
|
||||||
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||||
{% elseif monitor.monitorType == "tvseason" %}
|
{% elseif monitor.monitorType == "tvseason" %}
|
||||||
@@ -39,7 +52,7 @@
|
|||||||
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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" %}
|
{% if monitor.status == "New" %}
|
||||||
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
||||||
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
||||||
@@ -59,11 +72,11 @@
|
|||||||
{{ monitor.title }}
|
{{ monitor.title }}
|
||||||
</td>
|
</td>
|
||||||
{% if monitor|monitor_media_id != "-" %}
|
{% 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 }}
|
{{ monitor|monitor_media_id }}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% 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" %}
|
{% if monitor.monitorType == "tvshow" %}
|
||||||
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
<twig:StatusBadge color="blue" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||||
{% elseif monitor.monitorType == "tvseason" %}
|
{% elseif monitor.monitorType == "tvseason" %}
|
||||||
@@ -72,7 +85,7 @@
|
|||||||
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
<twig:StatusBadge color="fuchsia" number="300" text="black" status="{{ monitor.monitorType|monitor_type }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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" %}
|
{% if monitor.status == "New" %}
|
||||||
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
||||||
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
||||||
|
|||||||
@@ -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-2 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">
|
<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" />
|
<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>
|
</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">
|
<div class="modal-content mb-4">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|||||||
@@ -15,6 +15,4 @@
|
|||||||
<twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList>
|
<twig:DownloadList type="complete" :isWidget="false" :perPage="10"></twig:DownloadList>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<twig:PreviewModal id="previewModal" />
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
9
templates/index/test.html.twig
Normal file
9
templates/index/test.html.twig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block h2 %}Test Test{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div>
|
||||||
|
<!-- Well what are you doing here? -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
{% block h2 %}Monitors{% endblock %}
|
{% block h2 %}Monitors{% endblock %}
|
||||||
|
|
||||||
{% block action_buttons %}
|
{% block action_buttons %}
|
||||||
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false"
|
||||||
|
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||||
|
Upcoming Episodes
|
||||||
|
</a>
|
||||||
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
50
templates/monitor/upcoming-episodes.html.twig
Normal file
50
templates/monitor/upcoming-episodes.html.twig
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block h2 %}Upcoming Episodes{% endblock %}
|
||||||
|
|
||||||
|
{% block action_buttons %}
|
||||||
|
<a href="{{ path('app.monitor.upcoming-episodes') }}"
|
||||||
|
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||||
|
Upcoming Episodes
|
||||||
|
</a>
|
||||||
|
<twig:ActionButton action="monitorDispatch" text="Run Monitors" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{{ turbo_page_requires_reload() }}
|
||||||
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||||
|
<a href="{{ path('app.monitors.ical') }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
||||||
|
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||||
|
</a>
|
||||||
|
<div id="calendar" class="text-white">
|
||||||
|
</div>
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getView() {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
return 'listWeek';
|
||||||
|
} else {
|
||||||
|
return 'dayGridMonth';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
let data = await fetch('/api/monitor/upcoming-episodes');
|
||||||
|
data = (await data.json())['data'];
|
||||||
|
|
||||||
|
const calendarEl = document.getElementById('calendar');
|
||||||
|
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: getView(),
|
||||||
|
events: data['episodes'],
|
||||||
|
windowResize: function(arg) {
|
||||||
|
this.changeView(getView());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user