Compare commits
40 Commits
dev-tmdb-c
...
v0.34.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f11de70e0 | |||
| 4e06fe6636 | |||
| fd46abf58f | |||
| 2237a45d6f | |||
| 846de2c257 | |||
| d01b725435 | |||
| 7562597629 | |||
| deb0333635 | |||
| c8e190f9e8 | |||
| 538fde40fe | |||
| da7a267e2a | |||
| daf9b2c18b | |||
| d9e5e62f5d | |||
| d4fc7693e3 | |||
| 1263ad20a6 | |||
| e8764bb13b | |||
| a267bab86e | |||
| 9653189bff | |||
| 36836c4d36 | |||
| 61e4b25212 | |||
| 209266597e | |||
| ca89eff236 | |||
| 53da7a746b | |||
| 981699bc13 | |||
| 52f460ff62 | |||
| a42e0d4d1a | |||
| 3e4a2d9bb1 | |||
| af8a30826c | |||
| 09e1c75826 | |||
| f1b8b34359 | |||
| 6f9db68664 | |||
| aeb706b5af | |||
| 7918c260e5 | |||
| 38130ea0ec | |||
| da403958dc | |||
| c2bafabb20 | |||
| e6983aedf9 | |||
| ee076518b3 | |||
| a2f16398be | |||
| 0f03199eb4 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.phpunit.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.env.test
|
||||||
|
.gitignore
|
||||||
|
bolt.db
|
||||||
|
bash
|
||||||
|
build.xml
|
||||||
|
deploy.compose.yml
|
||||||
|
phpstan.dist.neon
|
||||||
|
phpunit.dist.xml
|
||||||
|
nomad.deploy.hcl
|
||||||
|
deployment.properties
|
||||||
15
Dockerfile
15
Dockerfile
@@ -1,19 +1,10 @@
|
|||||||
FROM dunglas/frankenphp:php8.4
|
FROM code.caldwell.digital/home/torsearch-base:php8.4
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ENV SERVER_NAME=":80"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
ENV APP_VERSION="0.0.1"
|
ENV APP_VERSION="0.0.0-dev"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
RUN apt update && apt install -y wget
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
|
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
|
||||||
|
|
||||||
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||||
@@ -13,34 +13,7 @@ export default class extends Controller {
|
|||||||
tmdbId: String,
|
tmdbId: String,
|
||||||
imdbId: String,
|
imdbId: String,
|
||||||
title: String,
|
title: String,
|
||||||
}
|
season: Number,
|
||||||
|
|
||||||
initialize() {
|
|
||||||
// Called once when the controller is first instantiated (per element)
|
|
||||||
|
|
||||||
// Here you can initialize variables, create scoped callables for event
|
|
||||||
// listeners, instantiate external libraries, etc.
|
|
||||||
// this._fooBar = this.fooBar.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
// Called every time the controller is connected to the DOM
|
|
||||||
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
|
|
||||||
|
|
||||||
// Here you can add event listeners on the element or target elements,
|
|
||||||
// add or remove classes, attributes, dispatch custom events, etc.
|
|
||||||
// this.fooTarget.addEventListener('click', this._fooBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
@@ -53,34 +26,13 @@ export default class extends Controller {
|
|||||||
imdbId: this.imdbIdValue,
|
imdbId: this.imdbIdValue,
|
||||||
title: this.titleValue,
|
title: this.titleValue,
|
||||||
monitorType: 'tvshows',
|
monitorType: 'tvshows',
|
||||||
|
season: this.seasonValue
|
||||||
});
|
});
|
||||||
if (this.hasDialogOutlet) {
|
if (this.hasDialogOutlet) {
|
||||||
this.dialogOutlet.close();
|
this.dialogOutlet.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async monitorSeason() {
|
|
||||||
await this.makeMonitor({
|
|
||||||
tmdbId: this.tmdbIdValue,
|
|
||||||
imdbId: this.imdbIdValue,
|
|
||||||
title: this.titleValue,
|
|
||||||
monitorType: 'tvseason',
|
|
||||||
season: this.resultFilterOutlet.activeFilter['season'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async monitorEpisode() {
|
|
||||||
// ToDo: figure out how to set episode
|
|
||||||
await this.makeMonitor({
|
|
||||||
tmdbId: this.tmdbIdValue,
|
|
||||||
imdbId: this.imdbIdValue,
|
|
||||||
title: this.titleValue,
|
|
||||||
monitorType: 'tvepisode',
|
|
||||||
season: this.resultFilterOutlet.activeFilter['season'],
|
|
||||||
episode: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeMonitor(body) {
|
async makeMonitor(body) {
|
||||||
const response = await fetch('/api/monitor', {
|
const response = await fetch('/api/monitor', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -90,7 +42,6 @@ export default class extends Controller {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default class extends Controller {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static targets = ['list']
|
static targets = ['list']
|
||||||
static outlets = ['loading-icon']
|
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
optionsLoaded = false
|
optionsLoaded = false
|
||||||
@@ -28,7 +27,6 @@ export default class extends Controller {
|
|||||||
this.options = this.element.querySelectorAll('tbody tr');
|
this.options = this.element.querySelectorAll('tbody tr');
|
||||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||||
this.resultCountEl.innerText = this.options.length;
|
this.resultCountEl.innerText = this.options.length;
|
||||||
this.loadingIconOutlet.toggleIcon();
|
|
||||||
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
|
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default class extends Controller {
|
|||||||
defaultOptions = '<option value="-">-</option>';
|
defaultOptions = '<option value="-">-</option>';
|
||||||
|
|
||||||
static outlets = ['tv-episode-list']
|
static outlets = ['tv-episode-list']
|
||||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected']
|
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected', 'currentSeason']
|
||||||
static values = {
|
static values = {
|
||||||
'imdbId': String,
|
'imdbId': String,
|
||||||
'media-type': String,
|
'media-type': String,
|
||||||
@@ -32,7 +32,6 @@ export default class extends Controller {
|
|||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
await this.setInitialFilter();
|
await this.setInitialFilter();
|
||||||
this.setTimerToStopLoadingIcon();
|
|
||||||
this.element.filterResults = this.filter.bind(this);
|
this.element.filterResults = this.filter.bind(this);
|
||||||
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
|
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
|
||||||
}
|
}
|
||||||
@@ -48,10 +47,6 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimerToStopLoadingIcon() {
|
|
||||||
setTimeout(() => this.loadingIconTarget.hideIcon(), 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event is fired from movies/tvshows controllers to populate this data
|
// Event is fired from movies/tvshows controllers to populate this data
|
||||||
async loadOptions({detail: { options }}) {
|
async loadOptions({detail: { options }}) {
|
||||||
await options.forEach((option) => {
|
await options.forEach((option) => {
|
||||||
@@ -99,7 +94,9 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSeason(event) {
|
setSeason(event) {
|
||||||
|
console.log('hurrrr');
|
||||||
this.tvEpisodeListOutlet.setSeason(event.target.value);
|
this.tvEpisodeListOutlet.setSeason(event.target.value);
|
||||||
|
this.currentSeasonTarget.innerText = event.target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSeason() {
|
downloadSeason() {
|
||||||
|
|||||||
@@ -38,21 +38,21 @@ export default class extends Controller {
|
|||||||
return `
|
return `
|
||||||
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
|
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
|
||||||
</span>
|
</span>`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
event.detail.options.render.option = (data, escape) => {
|
event.detail.options.render.option = (data, escape) => {
|
||||||
if (data.data.description.length > 60) {
|
console.log(data);
|
||||||
data.data.description = data.data.description.substring(0, 107) + "...";
|
if (data.data.overview.length > 60) {
|
||||||
|
data.data.overview = data.data.overview.substring(0, 107) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="flex flex-row">
|
return `<div class="flex flex-row">
|
||||||
<img src="${data.data.poster}" class="w-16 rounded-md">
|
<img src="${data.data.poster}" class="w-16 rounded-md">
|
||||||
<div class="p-2 flex flex-col">
|
<div class="p-2 flex flex-col">
|
||||||
<h2>${data.data.title}</h2>
|
<h2>${data.data.title}</h2>
|
||||||
<p class="max-w-[60ch] text-wrap">${data.data.description}</p>
|
<p class="max-w-[60ch] text-wrap">${data.data.overview}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>`;
|
||||||
`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default class extends Controller {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static targets = ['list', 'count', 'episodeSelector',]
|
static targets = ['list', 'count', 'episodeSelector',]
|
||||||
static outlets = ['loading-icon']
|
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
@@ -35,6 +34,5 @@ export default class extends Controller {
|
|||||||
this.countTarget.innerText = 0;
|
this.countTarget.innerText = 0;
|
||||||
this.episodeSelectorTarget.disabled = true;
|
this.episodeSelectorTarget.disabled = true;
|
||||||
}
|
}
|
||||||
this.loadingIconOutlet.increaseCount();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,10 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
@apply flex flex-col gap-1 justify-between;
|
@apply flex flex-col gap-1 justify-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** FullCalendar **/
|
||||||
|
#upcoming_episodes_calendar .fc-event-main .fc-event-title-container {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
.fc-col-header-cell {
|
.fc-col-header-cell {
|
||||||
@apply bg-orange-500/60 text-white;
|
@apply bg-orange-500/60 text-white;
|
||||||
}
|
}
|
||||||
|
|||||||
14
bash/build_base.sh
Executable file
14
bash/build_base.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
# torsearch-app is built from this base
|
||||||
|
export APP_FRANKENPHP_TAG=php8.4
|
||||||
|
|
||||||
|
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base:latest
|
||||||
|
|
||||||
|
# torsearch-worker & torsearch-scheduler are built from this base
|
||||||
|
export WORKER_FRANKENPHP_TAG=php8.4-alpine
|
||||||
|
|
||||||
|
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG}
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base-worker:latest
|
||||||
|
|
||||||
14
compose.yml
14
compose.yml
@@ -33,7 +33,11 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
build: .
|
build:
|
||||||
|
dockerfile: docker/Dockerfile.base.worker
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
FRANKENPHP_TAG: php8.4-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
@@ -41,11 +45,15 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume async -vv --time-limit=3600
|
command: php /app/bin/console messenger:consume async --time-limit=3600 -vv
|
||||||
|
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
build: .
|
build:
|
||||||
|
dockerfile: docker/Dockerfile.base.worker
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
FRANKENPHP_TAG: php8.4-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
|
|||||||
57
composer.lock
generated
57
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": "c133ccd27ac6a41256bdc69129c16546",
|
"content-hash": "e055bbbbe5836c92bb147b6dbb1d1d46",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -9467,6 +9467,57 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-05-12T14:48:23+00:00"
|
"time": "2025-05-12T14:48:23+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/serializer-pack",
|
||||||
|
"version": "v1.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/serializer-pack.git",
|
||||||
|
"reference": "2844d81a5fc86b617b82f44a8bfcaaba1d583eee"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/serializer-pack/zipball/2844d81a5fc86b617b82f44a8bfcaaba1d583eee",
|
||||||
|
"reference": "2844d81a5fc86b617b82f44a8bfcaaba1d583eee",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"phpdocumentor/reflection-docblock": "*",
|
||||||
|
"phpstan/phpdoc-parser": "*",
|
||||||
|
"symfony/property-access": "*",
|
||||||
|
"symfony/property-info": "*",
|
||||||
|
"symfony/serializer": "*"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/property-info": "<5.4",
|
||||||
|
"symfony/serializer": "<5.4"
|
||||||
|
},
|
||||||
|
"type": "symfony-pack",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "A pack for the Symfony serializer",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/symfony/serializer-pack/issues",
|
||||||
|
"source": "https://github.com/symfony/serializer-pack/tree/v1.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": "2023-06-03T13:55:25+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/service-contracts",
|
"name": "symfony/service-contracts",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
@@ -13347,7 +13398,7 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
@@ -13355,7 +13406,7 @@
|
|||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*"
|
"ext-iconv": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.4"
|
"php": "8.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ parameters:
|
|||||||
app.cache.redis.host.default: 'redis://redis'
|
app.cache.redis.host.default: 'redis://redis'
|
||||||
|
|
||||||
# Various configs
|
# Various configs
|
||||||
app.default.version: '0.dev'
|
app.default.version: '0.0.0-dev'
|
||||||
app.default.timezone: 'America/Chicago'
|
app.default.timezone: 'America/Chicago'
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: registry.caldwell.digital/home/torsearch-app:${TAG}
|
image: registry.caldwell.digital/home/torsearch-app:${TAG}
|
||||||
ports:
|
ports:
|
||||||
- '8001:80'
|
- "${SWARM_PORT}:80"
|
||||||
environment:
|
environment:
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
@@ -15,10 +15,6 @@ 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:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ templates
|
|||||||
var
|
var
|
||||||
vendor
|
vendor
|
||||||
build.xml
|
build.xml
|
||||||
|
.git
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
composer.json
|
composer.json
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
FROM dunglas/frankenphp
|
FROM code.caldwell.digital/home/torsearch-base:php8.4
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||||
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||||
|
|||||||
14
docker/Dockerfile.base.app
Normal file
14
docker/Dockerfile.base.app
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
ARG FRANKENPHP_TAG
|
||||||
|
|
||||||
|
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
||||||
|
|
||||||
|
ENV SERVER_NAME=":80"
|
||||||
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
|
|
||||||
|
RUN install-php-extensions \
|
||||||
|
pdo_mysql \
|
||||||
|
gd \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
opcache
|
||||||
19
docker/Dockerfile.base.worker
Normal file
19
docker/Dockerfile.base.worker
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
ARG FRANKENPHP_TAG
|
||||||
|
|
||||||
|
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
||||||
|
|
||||||
|
ENV SERVER_NAME=":80"
|
||||||
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
|
|
||||||
|
ARG APP_VERSION="0.dev"
|
||||||
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
|
RUN install-php-extensions \
|
||||||
|
pdo_mysql \
|
||||||
|
gd \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
opcache
|
||||||
|
|
||||||
|
RUN apk add --no-cache wget
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
ARG APP_VERSION
|
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
||||||
|
|
||||||
FROM code.caldwell.digital/torsearch/torsearch-app:${APP_VERSION}
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
FROM dunglas/frankenphp:php8.4-alpine
|
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ARG APP_VERSION="0.0.0-dev"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ENV APP_VERSION="${APP_VERSION}"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
||||||
|
|||||||
@@ -2,16 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Base;
|
namespace App\Base;
|
||||||
|
|
||||||
|
use App\Base\Dto\AppVersionDto;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
|
||||||
final class ConfigResolver
|
final class ConfigResolver
|
||||||
{
|
{
|
||||||
|
const SEMVER_REGEX = '/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/';
|
||||||
|
|
||||||
private array $messages = [];
|
private array $messages = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly DenormalizerInterface $denormalizer,
|
||||||
|
|
||||||
#[Autowire(param: 'app.url')]
|
#[Autowire(param: 'app.url')]
|
||||||
private readonly ?string $appUrl = null,
|
private readonly ?string $appUrl = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'app.version')]
|
||||||
|
private readonly ?string $appVersion = null,
|
||||||
|
|
||||||
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
||||||
private readonly ?string $realDebridApiKey = null,
|
private readonly ?string $realDebridApiKey = null,
|
||||||
|
|
||||||
@@ -92,6 +101,13 @@ final class ConfigResolver
|
|||||||
return $this->authOidcBypassFormLogin;
|
return $this->authOidcBypassFormLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAppVersion(): AppVersionDto
|
||||||
|
{
|
||||||
|
$matches = [];
|
||||||
|
preg_match(self::SEMVER_REGEX, $this->appVersion, $matches);
|
||||||
|
return $this->denormalizer->denormalize($matches, AppVersionDto::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function getAuthConfig(): array
|
public function getAuthConfig(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
24
src/Base/Dto/AppVersionDto.php
Normal file
24
src/Base/Dto/AppVersionDto.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Base\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||||
|
|
||||||
|
class AppVersionDto
|
||||||
|
{
|
||||||
|
#[SerializedPath('[1]')]
|
||||||
|
public string|int $major = 0;
|
||||||
|
#[SerializedPath('[2]')]
|
||||||
|
public string|int $minor = 0;
|
||||||
|
#[SerializedPath('[3]')]
|
||||||
|
public string|int $patch = 0;
|
||||||
|
#[SerializedPath('[4]')]
|
||||||
|
public ?string $pre = null;
|
||||||
|
#[SerializedPath('[5]')]
|
||||||
|
public ?string $build = null;
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return 'v' . $this->major . '.' . $this->minor . '.' . $this->patch . ($this->pre ? '-' . $this->pre : '') . ($this->build ? '+' . $this->build : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Base\Framework\Controller;
|
namespace App\Base\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Monitor\Action\Command\MonitorTvShowCommand;
|
||||||
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
@@ -46,8 +48,9 @@ final class IndexController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/test')]
|
#[Route('/test')]
|
||||||
public function monitorTvShow(): Response
|
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
||||||
{
|
{
|
||||||
|
// $handler->handle(new MonitorTvShowCommand(82));
|
||||||
return $this->render('index/test.html.twig', []);
|
return $this->render('index/test.html.twig', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,24 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||||
$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->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||||
|
|
||||||
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
if (null === $episodeData->airDate || "" === $episodeData->airDate) {
|
||||||
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode does not have an air date, skipping for now');
|
||||||
|
return new MonitorTvEpisodeResult(
|
||||||
|
status: 'OK',
|
||||||
|
result: [
|
||||||
|
'message' => 'No change',
|
||||||
|
'monitor' => $monitor,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
if (null === $monitor->getAirDate()) {
|
||||||
|
$monitor->setAirDate(Carbon::parse($episodeData->airDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Carbon::createFromTimestamp($episodeData->airDate) > 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(
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
|||||||
use App\Monitor\Action\Result\MonitorTvShowResult;
|
use App\Monitor\Action\Result\MonitorTvShowResult;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -29,7 +30,8 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
private MediaFiles $mediaFiles,
|
private MediaFiles $mediaFiles,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private TmdbClient $tmdb,
|
private TmdbClient $tmdb,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
@@ -39,78 +41,77 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
// Check current episodes
|
// Check current episodes
|
||||||
$downloadedEpisodes = $this->mediaFiles
|
$downloadedEpisodes = $this->mediaFiles
|
||||||
->getEpisodes($monitor->getTitle())
|
->getEpisodes($monitor->getTitle())
|
||||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
->map(fn($episode) => (object)(new PTN())->parse($episode))
|
||||||
->filter(fn ($episode) =>
|
->filter(fn($episode) => property_exists($episode, 'episode')
|
||||||
property_exists($episode, 'episode')
|
|
||||||
&& property_exists($episode, 'season')
|
&& property_exists($episode, 'season')
|
||||||
&& null !== $episode->episode
|
&& null !== $episode->episode
|
||||||
&& null !== $episode->season
|
&& null !== $episode->season
|
||||||
)
|
);
|
||||||
;
|
|
||||||
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
|
|
||||||
// Compare against list from TMDB
|
// Compare against list from TMDB
|
||||||
$episodesInShow = Map::from(
|
$episodesInShow = Map::from(
|
||||||
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
|
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
|
||||||
)->flat(1);
|
)->flat(1)
|
||||||
|
->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason())
|
||||||
|
->values();
|
||||||
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
||||||
|
|
||||||
$episodeMonitors = [];
|
$episodeMonitors = [];
|
||||||
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
|
// Dispatch Episode commands for each missing Episode
|
||||||
// Dispatch Episode commands for each missing Episode
|
foreach ($episodesInShow as $episode) {
|
||||||
foreach ($episodesInShow as $episode) {
|
/** @var TmdbEpisodeDto $episode */
|
||||||
// Only monitor future episodes
|
// Only monitor future episodes
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode['season_number'] . '" episode "' . $episode['episode_number'] . '"');
|
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode->seasonNumber . '" episode "' . $episode->episodeNumber . '"');
|
||||||
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
||||||
if (false === $episodeInFuture) {
|
if (false === $episodeInFuture) {
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the episode is already downloaded
|
|
||||||
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
|
||||||
if (true === $episodeExists) {
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing monitors
|
|
||||||
$monitorExists = $this->monitorExists($monitor, $episode);
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
|
||||||
if (true === $monitorExists) {
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the monitor
|
|
||||||
$episodeMonitor = (new Monitor())
|
|
||||||
->setParent($monitor)
|
|
||||||
->setUser($monitor->getUser())
|
|
||||||
->setTmdbId($monitor->getTmdbId())
|
|
||||||
->setImdbId($monitor->getImdbId())
|
|
||||||
->setTitle($monitor->getTitle())
|
|
||||||
->setMonitorType('tvepisode')
|
|
||||||
->setSeason($episode['season_number'])
|
|
||||||
->setEpisode($episode['episode_number'])
|
|
||||||
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
|
|
||||||
->setCreatedAt(new DateTimeImmutable())
|
|
||||||
->setSearchCount(0)
|
|
||||||
->setStatus('New');
|
|
||||||
|
|
||||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
|
||||||
$this->monitorRepository->getEntityManager()->flush();
|
|
||||||
|
|
||||||
$episodeMonitors[] = $episodeMonitor;
|
|
||||||
|
|
||||||
// Immediately run the monitor
|
|
||||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
|
||||||
$this->monitorTvEpisodeHandler->handle($command);
|
|
||||||
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the episode is already downloaded
|
||||||
|
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||||
|
if (true === $episodeExists) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing monitors
|
||||||
|
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||||
|
if (true === $monitorExists) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the monitor
|
||||||
|
$episodeMonitor = (new Monitor())
|
||||||
|
->setParent($monitor)
|
||||||
|
->setUser($monitor->getUser())
|
||||||
|
->setTmdbId($monitor->getTmdbId())
|
||||||
|
->setImdbId($monitor->getImdbId())
|
||||||
|
->setTitle($monitor->getTitle())
|
||||||
|
->setMonitorType('tvepisode')
|
||||||
|
->setSeason($episode->seasonNumber)
|
||||||
|
->setEpisode($episode->episodeNumber)
|
||||||
|
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
|
||||||
|
->setCreatedAt(new DateTimeImmutable())
|
||||||
|
->setSearchCount(0)
|
||||||
|
->setStatus('New');
|
||||||
|
|
||||||
|
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||||
|
$this->monitorRepository->getEntityManager()->flush();
|
||||||
|
|
||||||
|
$episodeMonitors[] = $episodeMonitor;
|
||||||
|
|
||||||
|
// Immediately run the monitor
|
||||||
|
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||||
|
$this->monitorTvEpisodeHandler->handle($command);
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the status to Active, so it will be re-executed.
|
// Set the status to Active, so it will be re-executed.
|
||||||
@@ -128,29 +129,31 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function episodeReleasedAfterMonitorCreated(string|DateTimeImmutable $monitorStartDate, array $episodeInShow): bool
|
private function episodeReleasedAfterMonitorCreated(
|
||||||
{
|
string|DateTimeImmutable $monitorStartDate,
|
||||||
|
TmdbEpisodeDto $episodeInShow
|
||||||
|
): bool {
|
||||||
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
||||||
$episodeAirDate = Carbon::parse($episodeInShow['air_date']);
|
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
||||||
return $episodeAirDate >= $monitorStartDate;
|
return $episodeAirDate >= $monitorStartDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
|
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
||||||
{
|
{
|
||||||
return $downloadedEpisodes->filter(
|
return $downloadedEpisodes->filter(
|
||||||
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
|
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||||
&& $episode->season === $episodeInShow['season_number']
|
&& $episode->season === $episodeInShow->seasonNumber
|
||||||
)->count() > 0;
|
)->count() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function monitorExists(Monitor $monitor, array $episode): bool
|
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
|
||||||
{
|
{
|
||||||
return $this->monitorRepository->findOneBy([
|
return $this->monitorRepository->findOneBy([
|
||||||
'imdbId' => $monitor->getImdbId(),
|
'imdbId' => $monitor->getImdbId(),
|
||||||
'title' => $monitor->getTitle(),
|
'title' => $monitor->getTitle(),
|
||||||
'monitorType' => 'tvepisode',
|
'monitorType' => 'tvepisode',
|
||||||
'season' => $episode['season_number'],
|
'season' => $episode->seasonNumber,
|
||||||
'episode' => $episode['episode_number'],
|
'episode' => $episode->episodeNumber,
|
||||||
'status' => ['New', 'Active', 'In Progress']
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
]) !== null;
|
]) !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class Monitor
|
|||||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||||
private ?self $parent = null;
|
private ?self $parent = null;
|
||||||
|
|
||||||
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent', cascade: ['remove'])]
|
||||||
private Collection $children;
|
private Collection $children;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class TmdbTvShowResultDenormalizer extends TmdbResultDenormalizer implements Den
|
|||||||
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
$result->year = (null !== $airDate) ? $airDate->format('Y') : null;
|
||||||
$result->mediaType = MediaType::TvShow->value;
|
$result->mediaType = MediaType::TvShow->value;
|
||||||
|
|
||||||
|
if (is_array($result->episodes)) {
|
||||||
|
$result->latestSeason = array_key_last($result->episodes);
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,13 +170,13 @@ class TmdbClient
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tvEpisodeDetails(string $tmdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, int $season, int $episode): TmdbResult|TmdbEpisodeDto|null
|
||||||
{
|
{
|
||||||
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
|
$result = $this->tvEpisodeRepository->getApi()->getEpisode($tmdbId, $season, $episode, ['append_to_response' => 'external_ids,credits']);
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$result,
|
$result,
|
||||||
MediaType::TvEpisode->value,
|
MediaType::TvEpisode->value,
|
||||||
$result['external_ids']['imdb_id']
|
$showImdbId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,5 +56,6 @@ class TmdbResult
|
|||||||
public ?array $producers = null,
|
public ?array $producers = null,
|
||||||
public ?int $runtime = null,
|
public ?int $runtime = null,
|
||||||
public ?int $numberSeasons = null,
|
public ?int $numberSeasons = null,
|
||||||
|
public ?int $latestSeason = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
|||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->season, $command->episode);
|
$media = $this->tmdb->tvEpisodeDetails($command->tmdbId, $command->imdbId, $command->season, $command->episode);
|
||||||
$parentShow = $this->tmdb->tvshowDetails($command->imdbId);
|
$parentShow = $this->tmdb->tvshowDetails($command->imdbId);
|
||||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Torrentio
|
|||||||
$results = $this->client->get($imdbCode, $cacheTags);
|
$results = $this->client->get($imdbCode, $cacheTags);
|
||||||
|
|
||||||
if (true === $parseResults) {
|
if (true === $parseResults) {
|
||||||
return $this->parse($results);
|
return $this->parse($results, $imdbCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
@@ -33,13 +33,13 @@ class Torrentio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (true === $parseResults) {
|
if (true === $parseResults) {
|
||||||
return $this->parse($results);
|
return $this->parse($results, $imdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parse(array $data): array
|
public function parse(array $data, string $imdbId): array
|
||||||
{
|
{
|
||||||
$results = [];
|
$results = [];
|
||||||
foreach ($data['streams'] as $stream) {
|
foreach ($data['streams'] as $stream) {
|
||||||
@@ -59,7 +59,8 @@ class Torrentio
|
|||||||
$result = ResultFactory::map(
|
$result = ResultFactory::map(
|
||||||
$stream['url'],
|
$stream['url'],
|
||||||
$stream['title'],
|
$stream['title'],
|
||||||
$bingeGroup
|
$bingeGroup,
|
||||||
|
$imdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
$results[] = $result;
|
$results[] = $result;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class ResultFactory
|
|||||||
public static function map(
|
public static function map(
|
||||||
string $url,
|
string $url,
|
||||||
string $title,
|
string $title,
|
||||||
string $bingeGroup = "-"
|
string $bingeGroup = "-",
|
||||||
|
string $imdbId = "-",
|
||||||
) {
|
) {
|
||||||
$ptn = (object) (new PTN())->parse($title);
|
$ptn = (object) (new PTN())->parse($title);
|
||||||
return new TorrentioResult(
|
return new TorrentioResult(
|
||||||
@@ -40,7 +41,8 @@ class ResultFactory
|
|||||||
self::setLanguages($title),
|
self::setLanguages($title),
|
||||||
self::setLanguageFlags($title),
|
self::setLanguageFlags($title),
|
||||||
false,
|
false,
|
||||||
uniqid()
|
uniqid(),
|
||||||
|
$imdbId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class TorrentioResult
|
|||||||
public ?array $languages = [],
|
public ?array $languages = [],
|
||||||
public ?string $languageFlags = "-",
|
public ?string $languageFlags = "-",
|
||||||
public ?bool $selected = false,
|
public ?bool $selected = false,
|
||||||
public ?string $localId = "-"
|
public ?string $localId = "-",
|
||||||
|
public ?string $imdbId = "-",
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Twig\Extensions;
|
namespace App\Twig\Extensions;
|
||||||
|
|
||||||
|
use App\Base\ConfigResolver;
|
||||||
|
use App\Base\Dto\AppVersionDto;
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
use App\Twig\Dto\EpisodeIdDto;
|
use App\Twig\Dto\EpisodeIdDto;
|
||||||
use ChrisUllyott\FileSize;
|
use ChrisUllyott\FileSize;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Twig\Attribute\AsTwigFilter;
|
use Twig\Attribute\AsTwigFilter;
|
||||||
use Twig\Attribute\AsTwigFunction;
|
use Twig\Attribute\AsTwigFunction;
|
||||||
|
|
||||||
@@ -13,9 +16,16 @@ class UtilExtension
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly ConfigResolver $config,
|
||||||
private readonly MediaFiles $mediaFiles,
|
private readonly MediaFiles $mediaFiles,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
#[AsTwigFunction('app_version')]
|
||||||
|
public function app_version(): AppVersionDto
|
||||||
|
{
|
||||||
|
return $this->config->getAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
#[AsTwigFunction('uniqid')]
|
#[AsTwigFunction('uniqid')]
|
||||||
public function uniqid(): string
|
public function uniqid(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
<div class="mt-2 inline-flex gap-4 justify-between text-white">
|
<div class="mt-2 inline-flex gap-4 justify-between text-white">
|
||||||
<a class="text-sm" href="{{ path('app_login') }}">Sign In</a>
|
<a class="text-sm" href="{{ path('app_login') }}">Sign In</a>
|
||||||
<span class="text-sm">v{{ version }}</span>
|
<span class="text-sm">{{ app_version() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -39,15 +39,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form_end(preferences_form) }}
|
{{ form_end(preferences_form) }}
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row justify-between">
|
<div class="flex flex-col-reverse md:flex-row justify-between">
|
||||||
<span
|
|
||||||
{{ stimulus_target('result-filter', 'loadingIcon') }}
|
|
||||||
{{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
|
|
||||||
class="loading-icon">
|
|
||||||
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
|
<p class="ml-2 mt-3 md:[margin-top:unset] md:self-center">Season <span data-result-filter-target="currentSeason" class="current-season">{{ results.season }}</span></p>
|
||||||
<div class="flex flex-row gap-2 justify-end px-8">
|
<div class="flex flex-row gap-2 justify-end px-8">
|
||||||
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 border border-green-500 bg-green-800/60 rounded-ms text-sm font-semibold" show_cancel show_submit>
|
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 border border-green-500 bg-green-800/60 rounded-ms text-sm font-semibold" show_cancel show_submit>
|
||||||
Downloading an entire season this way will use the filter from your
|
Downloading an entire season this way will use the filter from your
|
||||||
|
|||||||
@@ -14,18 +14,10 @@
|
|||||||
>
|
>
|
||||||
<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">
|
||||||
>
|
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
|
||||||
|
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type, season: episodeIdDto.season} %}
|
||||||
{% if monitor.monitorType == "movies" %}
|
<a href="{{ path('app_search_result', routeParams) }}"
|
||||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType} %}
|
|
||||||
{% set route = path('app_search_result', routeParams) %}
|
|
||||||
{% else %}
|
|
||||||
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
|
|
||||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType, season: episodeIdDto.season, episode: episodeIdDto.episode} %}
|
|
||||||
{% 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 dark:text-white">
|
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate dark:text-white">
|
||||||
{{ monitor.title }}
|
{{ monitor.title }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<p class="px-4 pt-1 inline-flex justify-center">
|
<p class="px-4 pt-1 inline-flex justify-center">
|
||||||
<small class="text-white text-xs">v{{ version|default('0.0') }}</small>
|
<small class="text-white text-xs">{{ app_version() }}</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
class="episode-list flex flex-col gap-4"
|
class="episode-list flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
|
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
|
||||||
|
{% if this.getEpisodes().items != null %}
|
||||||
{% for episode in this.getEpisodes().items %}
|
{% for episode in this.getEpisodes().items %}
|
||||||
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
|
<episode-container id="{{ episode_anchor(episode.seasonNumber, episode.episodeNumber) }}" class="results"
|
||||||
show-title="{{ this.title }}"
|
show-title="{{ this.title }}"
|
||||||
data-tv-results-loading-icon-outlet=".loading-icon"
|
|
||||||
data-download-button-outlet=".download-btn"
|
data-download-button-outlet=".download-btn"
|
||||||
{{ stimulus_controller('tv_results', {
|
{{ stimulus_controller('tv_results', {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<p>{{ episode.description|truncate }}</p>
|
<p>{{ episode.description|truncate }}</p>
|
||||||
<div class="text-xs font-bold">
|
<div class="text-xs font-bold">
|
||||||
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
|
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode.seasonNumber }} episode {{ episode.episodeNumber }}.">
|
||||||
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}></span> results
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 bg-gray-700 rounded-lg text-white" title='"{{ episode.name }}" aired on {{ episode.airDate|date(null, 'UTC') }}.'>
|
<span class="py-1 px-1.5 mr-1 grow-0 bg-gray-700 rounded-lg text-white" title='"{{ episode.name }}" aired on {{ episode.airDate|date(null, 'UTC') }}.'>
|
||||||
@@ -72,18 +72,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
||||||
<twig:Turbo:Frame id="results_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('app_torrentio_tvshows', {
|
<twig:Turbo:Frame loading="lazy" id="results_{{ episode_id(episode.seasonNumber, episode.episodeNumber) }}" src="{{ path('app_torrentio_tvshows', {
|
||||||
tmdbId: this.tmdbId,
|
tmdbId: this.tmdbId,
|
||||||
imdbId: this.imdbId,
|
imdbId: this.imdbId,
|
||||||
season: episode.seasonNumber,
|
season: episode.seasonNumber,
|
||||||
episode: episode.episodeNumber,
|
episode: episode.episodeNumber,
|
||||||
target: 'results_' ~ episode_id(episode.seasonNumber, episode.episodeNumber),
|
target: 'results_' ~ episode_id(episode.seasonNumber, episode.episodeNumber),
|
||||||
block: 'tvshow_results'
|
block: 'tvshow_results'
|
||||||
}) }}" />
|
}) }}">
|
||||||
|
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
|
||||||
|
</twig:Turbo:Frame>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</episode-container>
|
</episode-container>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% set paginator = this.episodes %}
|
{% set paginator = this.episodes %}
|
||||||
{% include 'partial/tv-episode-list-paginator.html.twig' %}
|
{% include 'partial/tv-episode-list-paginator.html.twig' %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" 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">
|
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" 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" />
|
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||||
</a>
|
</a>
|
||||||
<div id="calendar" class="text-white">
|
<div id="upcoming_episodes_calendar" class="text-white">
|
||||||
</div>
|
</div>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,15 +33,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const modal = document.getElementById('previewModal');
|
||||||
let data = await fetch('/api/monitor/upcoming-episodes');
|
let data = await fetch('/api/monitor/upcoming-episodes');
|
||||||
data = (await data.json())['data'];
|
data = (await data.json())['data'];
|
||||||
|
|
||||||
const calendarEl = document.getElementById('calendar');
|
const calendarEl = document.getElementById('upcoming_episodes_calendar');
|
||||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
initialView: getView(),
|
initialView: getView(),
|
||||||
events: data['episodes'],
|
events: data['episodes'],
|
||||||
windowResize: function(arg) {
|
windowResize: function(arg) {
|
||||||
this.changeView(getView());
|
this.changeView(getView());
|
||||||
|
},
|
||||||
|
eventClick: function (data) {
|
||||||
|
modal.display({
|
||||||
|
heading: data.event.title,
|
||||||
|
content: `<p>${data.event.startStr}</p>`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
calendar.render();
|
calendar.render();
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
||||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
{% if results.media.poster != null %}
|
{% if results.media.poster != null %}
|
||||||
<img class="w-full md:w-40 rounded-lg" src="{{ results.media.poster }}" />
|
<img class="w-full md:w-[12.5rem] rounded-lg" src="{{ results.media.poster }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full md:w-40 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
<div class="w-full md:w-[12.5rem] h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
<twig:ux:icon width="24" name="hugeicons:loading-01" />
|
<twig:ux:icon width="24" name="hugeicons:loading-01" />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
tmdbId: results.media.tmdbId,
|
tmdbId: results.media.tmdbId,
|
||||||
imdbId: results.media.imdbId,
|
imdbId: results.media.imdbId,
|
||||||
title: results.media.title,
|
title: results.media.title,
|
||||||
|
season: results.media.latestSeason,
|
||||||
})}}
|
})}}
|
||||||
data-monitor-button-result-filter-outlet="#filter"
|
data-monitor-button-result-filter-outlet="#filter"
|
||||||
data-monitor-button-dialog-outlet=".monitor-modal"
|
data-monitor-button-dialog-outlet=".monitor-modal"
|
||||||
@@ -83,10 +84,10 @@
|
|||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
<div class="flex flex-row justify-start items-end grow text-xs">
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-orange-500 rounded-lg text-white">
|
||||||
<span>{{ results.media.numberSeasons }}</span> season(s)
|
<span>{{ results.media.numberSeasons }}</span> season(s)
|
||||||
</span>
|
</span>
|
||||||
<span class="py-1 px-1.5 mr-1 grow-0 bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title='"{{ results.media.title }}" first aired on {{ results.media.premiereDate|date(null, 'UTC') }}.'>
|
||||||
{{ results.media.premiereDate|date(null, 'UTC') }}
|
{{ results.media.premiereDate|date(null, 'UTC') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,14 +126,15 @@
|
|||||||
{% if "movies" == results.media.mediaType %}
|
{% if "movies" == results.media.mediaType %}
|
||||||
<movie-container class="results"
|
<movie-container class="results"
|
||||||
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
||||||
data-movie-results-loading-icon-outlet=".loading-icon"
|
|
||||||
>
|
>
|
||||||
<twig:Turbo:Frame id="movie_results_frame" src="{{ path('app_torrentio_movies', {
|
<twig:Turbo:Frame id="movie_results_frame" src="{{ path('app_torrentio_movies', {
|
||||||
tmdbId: results.media.tmdbId,
|
tmdbId: results.media.tmdbId,
|
||||||
imdbId: results.media.imdbId,
|
imdbId: results.media.imdbId,
|
||||||
target: 'movie_results_frame',
|
target: 'movie_results_frame',
|
||||||
block: 'movie_results'
|
block: 'movie_results'
|
||||||
}) }}" />
|
}) }}">
|
||||||
|
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" title="Loading download options for {{ results.media.title }}" />
|
||||||
|
</twig:Turbo:Frame>
|
||||||
</movie-container>
|
</movie-container>
|
||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
@@ -151,8 +153,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
||||||
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
||||||
{% for media in results.relatedMedia %}
|
{% for media in results.relatedMedia %}
|
||||||
<twig:Poster imdbId="{{ media.imdbId }}"
|
<twig:Poster imdbId="{{ media.imdbId }}"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
provider="{{ result.provider }}"
|
provider="{{ result.provider }}"
|
||||||
languages="{{ result.languages|json_encode }}"
|
languages="{{ result.languages|json_encode }}"
|
||||||
media-type="{{ results.media.mediaType }}"
|
media-type="{{ results.media.mediaType }}"
|
||||||
imdb-id="{{ results.media.imdbId }}"
|
imdb-id="{{ result.imdbId }}"
|
||||||
filename="{{ result.filename }}"
|
filename="{{ result.filename }}"
|
||||||
data-local-id="{{ result.localId }}"
|
data-local-id="{{ result.localId }}"
|
||||||
{% if "tvshows" == results.media.mediaType %}
|
{% if "tvshows" == results.media.mediaType %}
|
||||||
|
|||||||
Reference in New Issue
Block a user