Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da0eab652b | ||
|
|
17de41dc57 | ||
|
|
d2eaccaf93 | ||
|
|
37e13347b2 | ||
|
|
7dd40b4525 | ||
|
|
2315b995e0 | ||
|
|
83401f2a7a | ||
|
|
50bcb4e1df | ||
|
|
ea569b480d | ||
|
|
50ec0c1d6f | ||
|
|
4ae70115b5 | ||
|
|
f4982af991 | ||
|
|
f253b33910 | ||
|
|
ec0d2a198c | ||
|
|
1f1c6f775f | ||
|
|
cd14a197aa | ||
|
|
a9031df3c3 | ||
|
|
55ab9d840e | ||
|
|
3001e85715 | ||
|
|
f4125cc37c | ||
|
|
a3408d9852 | ||
|
|
0048423a46 | ||
|
|
2468e4d5b6 | ||
|
|
445224d368 | ||
|
|
9a660279be | ||
|
|
c1adedf74d | ||
|
|
9a0e7fce26 | ||
|
|
d90b4d7863 | ||
|
|
2860d2e949 | ||
|
|
ad2bbfd48c | ||
|
|
5e306c6740 | ||
|
|
56129de3f9 | ||
|
|
22b2b46da5 | ||
|
|
7cc48ffc73 | ||
|
|
adb79db8f5 | ||
|
|
9ca87af938 | ||
|
|
b01840b111 | ||
|
|
1759b6dfdc | ||
|
|
2d3bc35e45 | ||
|
|
69f07b57ce | ||
|
|
b86028acee | ||
|
|
b67781fe23 | ||
|
|
6920b82684 |
@@ -12,3 +12,4 @@ phpstan.dist.neon
|
|||||||
phpunit.dist.xml
|
phpunit.dist.xml
|
||||||
nomad.deploy.hcl
|
nomad.deploy.hcl
|
||||||
deployment.properties
|
deployment.properties
|
||||||
|
var/download/*
|
||||||
|
|||||||
10
.env
10
.env
@@ -58,3 +58,13 @@ OIDC_BYPASS_FORM_LOGIN=false
|
|||||||
|
|
||||||
NOTIFICATION_TRANSPORT=
|
NOTIFICATION_TRANSPORT=
|
||||||
NTFY_DSN=
|
NTFY_DSN=
|
||||||
|
|
||||||
|
###> sentry/sentry-symfony ###
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_JS_URL=
|
||||||
|
###< sentry/sentry-symfony ###
|
||||||
|
|
||||||
|
# TMDB 'with_original_language' option
|
||||||
|
# - only include media originally
|
||||||
|
# produced in this language
|
||||||
|
TMDB_ORIGINAL_LANGUAGE=en
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
|||||||
<th class="px-4 py-2">
|
<th class="px-4 py-2">
|
||||||
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
|
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
|
||||||
</th>
|
</th>
|
||||||
|
<th class="px-4 py-2">
|
||||||
|
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Air Date</div>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -107,6 +110,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
|||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
|
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('air-date') ?? "-"}</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export default class PreviewContentDialog extends HTMLDialogElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display({ heading, content }) {
|
display({ heading, content }) {
|
||||||
|
if (this.hasAttribute('mdWidth')) {
|
||||||
|
this.style.width = this.getAttribute('mdWidth');
|
||||||
|
}
|
||||||
this.setHeading(heading);
|
this.setHeading(heading);
|
||||||
this.setContent(content);
|
this.setContent(content);
|
||||||
this.showModal();
|
this.showModal();
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null !== content && undefined !== content && "" !== content) {
|
if (null !== content && undefined !== content && "" !== content) {
|
||||||
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
|
if (['', null, undefined].includes(monitor.getAttribute('parent-id'))) {
|
||||||
|
window.location.href = `/monitors/${monitor.getAttribute('monitor-id')}`;
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
1
assets/icons/bi/camera-video.svg
Normal file
1
assets/icons/bi/camera-video.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2zm11.5 5.175l3.5 1.556V4.269l-3.5 1.556zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 374 B |
1
assets/icons/bi/cloud-download.svg
Normal file
1
assets/icons/bi/cloud-download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773C16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318C1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593c.143-.863.698-1.723 1.464-2.383"/><path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 720 B |
1
assets/icons/bi/download.svg
Normal file
1
assets/icons/bi/download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 377 B |
1
assets/icons/fluent-mdl2/back.svg
Normal file
1
assets/icons/fluent-mdl2/back.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path fill="currentColor" d="M2048 1088H250l787 787l-90 90L6 1024L947 83l90 90l-787 787h1798z"/></svg>
|
||||||
|
After Width: | Height: | Size: 167 B |
@@ -1,14 +1,22 @@
|
|||||||
# torsearch-app is built from this base
|
# torsearch-app is built from this base
|
||||||
export APP_FRANKENPHP_TAG=php8.4
|
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 buildx build --platform=linux/amd64,linux/arm64 -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 build -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:${APP_FRANKENPHP_TAG}
|
||||||
docker push code.caldwell.digital/home/torsearch-base:latest
|
docker push code.caldwell.digital/home/torsearch-base:latest
|
||||||
|
|
||||||
# torsearch-worker & torsearch-scheduler are built from this base
|
# torsearch-worker & torsearch-scheduler are built from this base
|
||||||
export WORKER_FRANKENPHP_TAG=php8.4-alpine
|
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 buildx build --platform=linux/amd64,linux/arm64 -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 build -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:${WORKER_FRANKENPHP_TAG}
|
||||||
docker push code.caldwell.digital/home/torsearch-base-worker:latest
|
docker push code.caldwell.digital/home/torsearch-base-worker:latest
|
||||||
|
|
||||||
|
# torsearch-worker-supervisord
|
||||||
|
export ALPINE_VERSION=3.22
|
||||||
|
|
||||||
|
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" .
|
||||||
|
docker build -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" .
|
||||||
|
docker push code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
|
||||||
|
|||||||
17
compose.yml
17
compose.yml
@@ -34,10 +34,8 @@ services:
|
|||||||
|
|
||||||
worker:
|
worker:
|
||||||
build:
|
build:
|
||||||
dockerfile: docker/Dockerfile.base.worker
|
dockerfile: docker/Dockerfile.worker
|
||||||
context: .
|
context: .
|
||||||
args:
|
|
||||||
FRANKENPHP_TAG: php8.4-alpine
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
@@ -45,22 +43,19 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume async --time-limit=3600 -vv
|
|
||||||
|
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
build:
|
build:
|
||||||
dockerfile: docker/Dockerfile.base.worker
|
dockerfile: docker/Dockerfile.scheduler
|
||||||
context: .
|
context: .
|
||||||
args:
|
|
||||||
FRANKENPHP_TAG: php8.4-alpine
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD:/app
|
- $PWD:/app
|
||||||
|
- $PWD/var/download:/var/download
|
||||||
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
command: php /app/bin/console messenger:consume scheduler_monitor -vv
|
WORKER_MONITOR: 2
|
||||||
tty: true
|
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
@@ -96,6 +91,8 @@ services:
|
|||||||
image: adminer
|
image: adminer
|
||||||
ports:
|
ports:
|
||||||
- "8081:8080"
|
- "8081:8080"
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: database
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"predis/predis": "^2.4",
|
"predis/predis": "^2.4",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
|
"sentry/sentry-symfony": "^5.6",
|
||||||
"spatie/icalendar-generator": "^3.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",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"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/monolog-bundle": "^3.10",
|
||||||
"symfony/notifier": "7.3.*",
|
"symfony/notifier": "7.3.*",
|
||||||
"symfony/ntfy-notifier": "7.3.*",
|
"symfony/ntfy-notifier": "7.3.*",
|
||||||
"symfony/object-mapper": "7.3.*",
|
"symfony/object-mapper": "7.3.*",
|
||||||
|
|||||||
605
composer.lock
generated
605
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": "952def2c32d975032ac0061e5aa37319",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -2552,6 +2552,66 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-27T12:30:47+00:00"
|
"time": "2025-03-27T12:30:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jean85/pretty-package-versions",
|
||||||
|
"version": "2.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Jean85/pretty-package-versions.git",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-runtime-api": "^2.1.0",
|
||||||
|
"php": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"jean85/composer-provided-replaced-stub-package": "^1.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpunit/phpunit": "^7.5|^8.5|^9.6",
|
||||||
|
"rector/rector": "^2.0",
|
||||||
|
"vimeo/psalm": "^4.3 || ^5.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Jean85\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Alessandro Lai",
|
||||||
|
"email": "alessandro.lai85@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to get pretty versions strings of installed dependencies",
|
||||||
|
"keywords": [
|
||||||
|
"composer",
|
||||||
|
"package",
|
||||||
|
"release",
|
||||||
|
"versions"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Jean85/pretty-package-versions/issues",
|
||||||
|
"source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
|
||||||
|
},
|
||||||
|
"time": "2025-03-19T14:43:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "lcobucci/jwt",
|
"name": "lcobucci/jwt",
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
@@ -2681,6 +2741,109 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-02-06T08:48:15+00:00"
|
"time": "2025-02-06T08:48:15+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "monolog/monolog",
|
||||||
|
"version": "3.9.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Seldaek/monolog.git",
|
||||||
|
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
||||||
|
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/log": "^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"psr/log-implementation": "3.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"aws/aws-sdk-php": "^3.0",
|
||||||
|
"doctrine/couchdb": "~1.0@dev",
|
||||||
|
"elasticsearch/elasticsearch": "^7 || ^8",
|
||||||
|
"ext-json": "*",
|
||||||
|
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.4.5",
|
||||||
|
"guzzlehttp/psr7": "^2.2",
|
||||||
|
"mongodb/mongodb": "^1.8",
|
||||||
|
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
||||||
|
"php-console/php-console": "^3.1.8",
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2",
|
||||||
|
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
|
||||||
|
"predis/predis": "^1.1 || ^2",
|
||||||
|
"rollbar/rollbar": "^4.0",
|
||||||
|
"ruflin/elastica": "^7 || ^8",
|
||||||
|
"symfony/mailer": "^5.4 || ^6",
|
||||||
|
"symfony/mime": "^5.4 || ^6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
|
||||||
|
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
|
||||||
|
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
|
||||||
|
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
|
||||||
|
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
|
||||||
|
"ext-mbstring": "Allow to work properly with unicode symbols",
|
||||||
|
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
|
||||||
|
"ext-openssl": "Required to send log messages using SSL",
|
||||||
|
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
|
||||||
|
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
|
||||||
|
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
|
||||||
|
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
|
||||||
|
"rollbar/rollbar": "Allow sending log messages to Rollbar",
|
||||||
|
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Monolog\\": "src/Monolog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "https://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
|
||||||
|
"homepage": "https://github.com/Seldaek/monolog",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||||
|
"source": "https://github.com/Seldaek/monolog/tree/3.9.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Seldaek",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-03-24T10:02:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@@ -4757,6 +4920,196 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-12-12T12:06:11+00:00"
|
"time": "2023-12-12T12:06:11+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sentry/sentry",
|
||||||
|
"version": "4.17.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/getsentry/sentry-php.git",
|
||||||
|
"reference": "5c696b8de57e841a2bf3b6f6eecfd99acfdda80c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/5c696b8de57e841a2bf3b6f6eecfd99acfdda80c",
|
||||||
|
"reference": "5c696b8de57e841a2bf3b6f6eecfd99acfdda80c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"guzzlehttp/psr7": "^1.8.4|^2.1.1",
|
||||||
|
"jean85/pretty-package-versions": "^1.5|^2.0.4",
|
||||||
|
"php": "^7.2|^8.0",
|
||||||
|
"psr/log": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"raven/raven": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.4",
|
||||||
|
"guzzlehttp/promises": "^2.0.3",
|
||||||
|
"guzzlehttp/psr7": "^1.8.4|^2.1.1",
|
||||||
|
"monolog/monolog": "^1.6|^2.0|^3.0",
|
||||||
|
"phpbench/phpbench": "^1.0",
|
||||||
|
"phpstan/phpstan": "^1.3",
|
||||||
|
"phpunit/phpunit": "^8.5|^9.6",
|
||||||
|
"vimeo/psalm": "^4.17"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sentry\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sentry",
|
||||||
|
"email": "accounts@sentry.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP SDK for Sentry (http://sentry.io)",
|
||||||
|
"homepage": "http://sentry.io",
|
||||||
|
"keywords": [
|
||||||
|
"crash-reporting",
|
||||||
|
"crash-reports",
|
||||||
|
"error-handler",
|
||||||
|
"error-monitoring",
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"profiling",
|
||||||
|
"sentry",
|
||||||
|
"tracing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/getsentry/sentry-php/issues",
|
||||||
|
"source": "https://github.com/getsentry/sentry-php/tree/4.17.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/pricing/",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-10-23T15:19:24+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sentry/sentry-symfony",
|
||||||
|
"version": "5.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/getsentry/sentry-symfony.git",
|
||||||
|
"reference": "9867751f5091b55d7e3a223f48d88e228132e073"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/9867751f5091b55d7e3a223f48d88e228132e073",
|
||||||
|
"reference": "9867751f5091b55d7e3a223f48d88e228132e073",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"guzzlehttp/psr7": "^2.1.1",
|
||||||
|
"jean85/pretty-package-versions": "^1.5||^2.0",
|
||||||
|
"php": "^7.2||^8.0",
|
||||||
|
"sentry/sentry": "^4.16.0",
|
||||||
|
"symfony/cache-contracts": "^1.1||^2.4||^3.0",
|
||||||
|
"symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/polyfill-php80": "^1.22",
|
||||||
|
"symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/dbal": "^2.13||^3.3||^4.0",
|
||||||
|
"doctrine/doctrine-bundle": "^2.6",
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.19||^3.40",
|
||||||
|
"masterminds/html5": "^2.8",
|
||||||
|
"phpstan/extension-installer": "^1.0",
|
||||||
|
"phpstan/phpstan": "1.12.5",
|
||||||
|
"phpstan/phpstan-phpunit": "1.4.0",
|
||||||
|
"phpstan/phpstan-symfony": "1.4.10",
|
||||||
|
"phpunit/phpunit": "^8.5.40||^9.6.21",
|
||||||
|
"symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/monolog-bundle": "^3.4",
|
||||||
|
"symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0",
|
||||||
|
"symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0",
|
||||||
|
"vimeo/psalm": "^4.3||^5.16.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.",
|
||||||
|
"monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.",
|
||||||
|
"symfony/cache": "Allow distributed tracing of cache pools using Sentry.",
|
||||||
|
"symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry."
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/aliases.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sentry\\SentryBundle\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sentry",
|
||||||
|
"email": "accounts@sentry.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony integration for Sentry (http://getsentry.com)",
|
||||||
|
"homepage": "http://getsentry.com",
|
||||||
|
"keywords": [
|
||||||
|
"errors",
|
||||||
|
"logging",
|
||||||
|
"sentry",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/getsentry/sentry-symfony/issues",
|
||||||
|
"source": "https://github.com/getsentry/sentry-symfony/tree/5.6.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/pricing/",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-09-24T13:41:01+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/icalendar-generator",
|
"name": "spatie/icalendar-generator",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -7637,6 +7990,169 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-02-19T08:51:26+00:00"
|
"time": "2025-02-19T08:51:26+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/monolog-bridge",
|
||||||
|
"version": "v7.3.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/monolog-bridge.git",
|
||||||
|
"reference": "c66a65049c75f3ddf03d73c8c9ed61405779ce47"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/c66a65049c75f3ddf03d73c8c9ed61405779ce47",
|
||||||
|
"reference": "c66a65049c75f3ddf03d73c8c9ed61405779ce47",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"monolog/monolog": "^3",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0",
|
||||||
|
"symfony/service-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/console": "<6.4",
|
||||||
|
"symfony/http-foundation": "<6.4",
|
||||||
|
"symfony/security-core": "<6.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/console": "^6.4|^7.0",
|
||||||
|
"symfony/http-client": "^6.4|^7.0",
|
||||||
|
"symfony/mailer": "^6.4|^7.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0",
|
||||||
|
"symfony/mime": "^6.4|^7.0",
|
||||||
|
"symfony/security-core": "^6.4|^7.0",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Bridge\\Monolog\\": ""
|
||||||
|
},
|
||||||
|
"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": "Provides integration for Monolog with various Symfony components",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/monolog-bridge/tree/v7.3.5"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-10-14T19:16:15+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/monolog-bundle",
|
||||||
|
"version": "v3.10.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/monolog-bundle.git",
|
||||||
|
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
|
||||||
|
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
|
||||||
|
"php": ">=7.2.5",
|
||||||
|
"symfony/config": "^5.4 || ^6.0 || ^7.0",
|
||||||
|
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
|
||||||
|
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
|
||||||
|
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/console": "^5.4 || ^6.0 || ^7.0",
|
||||||
|
"symfony/phpunit-bridge": "^6.3 || ^7.0",
|
||||||
|
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Bundle\\MonologBundle\\": ""
|
||||||
|
},
|
||||||
|
"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": "Symfony MonologBundle",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"logging"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/symfony/monolog-bundle/issues",
|
||||||
|
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.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-11-06T17:08:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/notifier",
|
"name": "symfony/notifier",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
@@ -8777,6 +9293,89 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-04-04T13:12:05+00:00"
|
"time": "2025-04-04T13:12:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/psr-http-message-bridge",
|
||||||
|
"version": "v7.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/psr-http-message-bridge.git",
|
||||||
|
"reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f",
|
||||||
|
"reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/http-message": "^1.0|^2.0",
|
||||||
|
"symfony/http-foundation": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"php-http/discovery": "<1.15",
|
||||||
|
"symfony/http-kernel": "<6.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"nyholm/psr7": "^1.1",
|
||||||
|
"php-http/discovery": "^1.15",
|
||||||
|
"psr/log": "^1.1.4|^2|^3",
|
||||||
|
"symfony/browser-kit": "^6.4|^7.0",
|
||||||
|
"symfony/config": "^6.4|^7.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0",
|
||||||
|
"symfony/framework-bundle": "^6.4|^7.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Bridge\\PsrHttpMessage\\": ""
|
||||||
|
},
|
||||||
|
"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": "PSR HTTP message bridge",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr-17",
|
||||||
|
"psr-7"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/psr-http-message-bridge/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": "2024-09-26T08:57:56+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/routing",
|
"name": "symfony/routing",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
@@ -13398,7 +13997,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": {
|
||||||
@@ -13406,7 +14005,7 @@
|
|||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*"
|
"ext-iconv": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.4"
|
"php": "8.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ return [
|
|||||||
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||||
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
|
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
|
||||||
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
|
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
|
||||||
|
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
|
||||||
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,26 @@ framework:
|
|||||||
max_retries: 1
|
max_retries: 1
|
||||||
multiplier: 1
|
multiplier: 1
|
||||||
|
|
||||||
|
download:
|
||||||
|
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||||
|
options:
|
||||||
|
use_notify: true
|
||||||
|
check_delayed_interval: 60000
|
||||||
|
queue_name: download
|
||||||
|
retry_strategy:
|
||||||
|
max_retries: 3
|
||||||
|
multiplier: 1
|
||||||
|
|
||||||
|
monitor:
|
||||||
|
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||||
|
options:
|
||||||
|
use_notify: true
|
||||||
|
check_delayed_interval: 60000
|
||||||
|
queue_name: monitor
|
||||||
|
retry_strategy:
|
||||||
|
max_retries: 1
|
||||||
|
multiplier: 1
|
||||||
|
|
||||||
media_cache:
|
media_cache:
|
||||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||||
options:
|
options:
|
||||||
@@ -36,12 +56,12 @@ framework:
|
|||||||
routing:
|
routing:
|
||||||
# Route your messages to the transports
|
# Route your messages to the transports
|
||||||
# 'App\Message\YourMessage': async
|
# 'App\Message\YourMessage': async
|
||||||
'App\Download\Action\Command\DownloadMediaCommand': async
|
'App\Download\Action\Command\DownloadMediaCommand': download
|
||||||
'App\Download\Action\Command\DownloadSeasonCommand': async
|
'App\Download\Action\Command\DownloadSeasonCommand': async
|
||||||
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': async
|
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': monitor
|
||||||
'App\Monitor\Action\Command\MonitorTvSeasonCommand': async
|
'App\Monitor\Action\Command\MonitorTvSeasonCommand': monitor
|
||||||
'App\Monitor\Action\Command\MonitorTvShowCommand': async
|
'App\Monitor\Action\Command\MonitorTvShowCommand': monitor
|
||||||
'App\Monitor\Action\Command\MonitorMovieCommand': async
|
'App\Monitor\Action\Command\MonitorMovieCommand': monitor
|
||||||
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': media_cache
|
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': media_cache
|
||||||
|
|
||||||
# when@test:
|
# when@test:
|
||||||
|
|||||||
63
config/packages/monolog.yaml
Normal file
63
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
monolog:
|
||||||
|
channels:
|
||||||
|
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||||
|
|
||||||
|
when@dev:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!event"]
|
||||||
|
# uncomment to get logging in your browser
|
||||||
|
# you may have to allow bigger header sizes in your Web server configuration
|
||||||
|
#firephp:
|
||||||
|
# type: firephp
|
||||||
|
# level: info
|
||||||
|
#chromephp:
|
||||||
|
# type: chromephp
|
||||||
|
# level: info
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine", "!console"]
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!event"]
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!deprecation"]
|
||||||
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: php://stderr
|
||||||
|
level: debug
|
||||||
|
formatter: monolog.formatter.json
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine"]
|
||||||
|
deprecation:
|
||||||
|
type: stream
|
||||||
|
channels: [deprecation]
|
||||||
|
path: php://stderr
|
||||||
|
formatter: monolog.formatter.json
|
||||||
40
config/packages/sentry.yaml
Normal file
40
config/packages/sentry.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
when@prod:
|
||||||
|
sentry:
|
||||||
|
register_error_listener: true # Disables the ErrorListener to avoid duplicated log in sentry
|
||||||
|
register_error_handler: true # Disables the ErrorListener, ExceptionListener and FatalErrorListener integrations of the base PHP SDK
|
||||||
|
|
||||||
|
options:
|
||||||
|
enable_logs: true
|
||||||
|
traces_sample_rate: 1
|
||||||
|
profiles_sample_rate: 1
|
||||||
|
attach_stacktrace: true
|
||||||
|
|
||||||
|
tracing:
|
||||||
|
enabled: true
|
||||||
|
dbal: # DB queries
|
||||||
|
enabled: true
|
||||||
|
cache: # cache pools
|
||||||
|
enabled: true
|
||||||
|
twig: # templating engine
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
# (Optionally) Configure the breadcrumb handler as a service (needed for the breadcrumb Monolog handler)
|
||||||
|
Sentry\Monolog\BreadcrumbHandler:
|
||||||
|
arguments:
|
||||||
|
- '@Sentry\State\HubInterface'
|
||||||
|
- !php/const Monolog\Logger::INFO # Configures the level of messages to capture as breadcrumbs
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
# (Optionally) Register the breadcrumb handler as a Monolog handler
|
||||||
|
sentry_breadcrumbs:
|
||||||
|
type: service
|
||||||
|
name: sentry_breadcrumbs
|
||||||
|
id: Sentry\Monolog\BreadcrumbHandler
|
||||||
|
# Register the handler as a Monolog handler to capture messages as events
|
||||||
|
sentry:
|
||||||
|
type: sentry
|
||||||
|
level: !php/const Monolog\Logger::ERROR # Configures the level of messages to capture as events
|
||||||
|
hub_id: Sentry\State\HubInterface
|
||||||
|
fill_extra_context: true # Enables sending monolog context to Sentry
|
||||||
|
process_psr_3_messages: false # Disables the resolution of PSR-3 placeholders in reported messages
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
twig:
|
twig:
|
||||||
globals:
|
globals:
|
||||||
version: '%app.version%'
|
version: '%app.version%'
|
||||||
|
sentry_javascript_url: '%sentry.javascript_url%'
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
date:
|
date:
|
||||||
format: 'm/d/Y'
|
format: 'm/d/Y'
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ parameters:
|
|||||||
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
||||||
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
sentry.dsn: '%env(SENTRY_DSN)%'
|
||||||
|
sentry.javascript_url: '%env(SENTRY_JS_URL)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ services:
|
|||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||||
- mercure_data:/data
|
- mercure_data:/data
|
||||||
- mercure_config:/config
|
- mercure_config:/config
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
|
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
@@ -22,8 +20,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/media/downloads/movies:/var/download/movies
|
- /mnt/media/downloads/movies:/var/download/movies
|
||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||||
restart: always
|
|
||||||
command: -vv --time-limit=3600 --limit=10
|
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 2
|
replicas: 2
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -35,23 +31,10 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/media/downloads/movies:/var/download/movies
|
- /mnt/media/downloads/movies:/var/download/movies
|
||||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||||
restart: always
|
|
||||||
command: -vv
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:latest
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
command: redis-server --maxmemory 512MB
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql:
|
|
||||||
mercure_config:
|
mercure_config:
|
||||||
mercure_data:
|
mercure_data:
|
||||||
redis_data:
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,40 @@
|
|||||||
ARG FRANKENPHP_TAG
|
#####
|
||||||
|
# This version of Torsearch runs the scheduler, downloader, and worker
|
||||||
|
# in a single container. Each process is managerd by supervisord
|
||||||
|
# and can be configured via environment variables and more
|
||||||
|
# than one of these containers cans till be run.
|
||||||
|
#####
|
||||||
|
ARG ALPINE_VERSION="3.22"
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS build_stage
|
||||||
|
|
||||||
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
RUN apk add --no-cache \
|
||||||
|
curl \
|
||||||
|
php84 \
|
||||||
|
php84-ctype \
|
||||||
|
php84-curl \
|
||||||
|
php84-dom \
|
||||||
|
php84-fileinfo \
|
||||||
|
php84-fpm \
|
||||||
|
php84-gd \
|
||||||
|
php84-mbstring \
|
||||||
|
php84-mysqli \
|
||||||
|
php84-opcache \
|
||||||
|
php84-openssl \
|
||||||
|
php84-pdo_mysql \
|
||||||
|
php84-tokenizer \
|
||||||
|
php84-xml \
|
||||||
|
supervisor
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
RUN ln -s /usr/bin/php84 /usr/bin/php
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
RUN mkdir -p /etc/supervisor/conf.d
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
|
||||||
|
|
||||||
RUN install-php-extensions \
|
# We start supervisord and supervisord starts
|
||||||
pdo_mysql \
|
# respective service in the configuration file.
|
||||||
gd \
|
ENTRYPOINT ["/app/bin/console", "init:worker"]
|
||||||
intl \
|
|
||||||
zip \
|
# Message transports can be enabled by passing them as command options.
|
||||||
opcache
|
# Inlcluding a number with the option will start that amount of processes
|
||||||
|
# for the transport. Not supplying a number will default to 1.
|
||||||
|
# --download --monitor OR --download 2 --monitor
|
||||||
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
###
|
||||||
|
# This version of Torsearch can run the scheduler, downloader, and
|
||||||
|
# worker in one container. Each process is managerd by supervisord
|
||||||
|
# and can be configured via environment variables, and more than
|
||||||
|
# one of these containers can still be run.
|
||||||
|
###
|
||||||
|
|
||||||
ARG APP_VERSION="0.0.0-dev"
|
# Default to latest, but should pass in a version
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ARG APP_VERSION="latest"
|
||||||
|
|
||||||
COPY . /app
|
# Set aside the actual app image so we can copy the app from there
|
||||||
|
FROM code.caldwell.digital/home/torsearch-app:${APP_VERSION} AS app_image
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
# Start with our base worker image
|
||||||
|
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
|
||||||
|
|
||||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0
|
# Set the APP_VERSION in the image
|
||||||
|
ENV APP_VERSION=${APP_VERSION}
|
||||||
|
|
||||||
|
# Copy the actual application code from the previously built app
|
||||||
|
COPY --chown=1000:1000 --from=app_image /app /app
|
||||||
|
|
||||||
|
# To retain backwards compatibility, default to async & download transports
|
||||||
|
CMD [ "--monitor" ]
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
|
###
|
||||||
|
# This version of Torsearch can run the scheduler, downloader, and
|
||||||
|
# worker in one container. Each process is managerd by supervisord
|
||||||
|
# and can be configured via environment variables, and more than
|
||||||
|
# one of these containers can still be run.
|
||||||
|
###
|
||||||
|
|
||||||
ARG APP_VERSION="0.0.0-dev"
|
# Default to latest, but should pass in a version
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
ARG APP_VERSION="latest"
|
||||||
|
|
||||||
COPY . /app
|
# Set aside the actual app image so we can copy the app from there
|
||||||
|
FROM code.caldwell.digital/home/torsearch-app:${APP_VERSION} AS app_image
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
# Start with our base worker image
|
||||||
|
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
|
||||||
|
|
||||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0
|
# Set the APP_VERSION in the image
|
||||||
|
ENV APP_VERSION=${APP_VERSION}
|
||||||
|
|
||||||
|
# Copy the actual application code from the previously built app
|
||||||
|
COPY --chown=1000:1000 --from=app_image /app /app
|
||||||
|
|
||||||
|
# To retain backwards compatibility, default to async & download transports
|
||||||
|
CMD [ "--async", "--download" ]
|
||||||
|
|||||||
10
docker/worker/async.conf
Normal file
10
docker/worker/async.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-worker]
|
||||||
|
command=/app/bin/console messenger:consume async -vv --time-limit 3600
|
||||||
|
numprocs=1
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
10
docker/worker/download.conf
Normal file
10
docker/worker/download.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-downloader]
|
||||||
|
command=/app/bin/console messenger:consume download -vv --time-limit 3600
|
||||||
|
numprocs=1
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
10
docker/worker/monitor.conf
Normal file
10
docker/worker/monitor.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-scheduler]
|
||||||
|
command=/app/bin/console messenger:consume scheduler_monitor monitor -vv --time-limit 21600
|
||||||
|
numprocs=1
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
5
docker/worker/supervisord.conf
Normal file
5
docker/worker/supervisord.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/run/supervisord.pid
|
||||||
@@ -23,9 +23,6 @@ final class Version20250831013403 extends AbstractMigration
|
|||||||
$this->addSql(<<<'SQL'
|
$this->addSql(<<<'SQL'
|
||||||
DROP TABLE IF EXISTS sessions
|
DROP TABLE IF EXISTS sessions
|
||||||
SQL);
|
SQL);
|
||||||
$this->addSql(<<<'SQL'
|
|
||||||
ALTER TABLE download CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL
|
|
||||||
SQL);
|
|
||||||
$this->addSql(<<<'SQL'
|
$this->addSql(<<<'SQL'
|
||||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
|
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
|
||||||
SQL);
|
SQL);
|
||||||
@@ -37,9 +34,6 @@ final class Version20250831013403 extends AbstractMigration
|
|||||||
$this->addSql(<<<'SQL'
|
$this->addSql(<<<'SQL'
|
||||||
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
|
||||||
SQL);
|
SQL);
|
||||||
$this->addSql(<<<'SQL'
|
|
||||||
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
|
|
||||||
SQL);
|
|
||||||
$this->addSql(<<<'SQL'
|
$this->addSql(<<<'SQL'
|
||||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
||||||
SQL);
|
SQL);
|
||||||
|
|||||||
35
migrations/Version20251102221034.php
Normal file
35
migrations/Version20251102221034.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 Version20251102221034 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 event_log ADD created_at DATETIME NULL, ADD updated_at DATETIME 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 event_log DROP created_at, DROP updated_at
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20251106045808.php
Normal file
34
migrations/Version20251106045808.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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 Version20251106045808 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE monitor ADD poster VARCHAR(1024) 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 poster
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,15 @@ final class ConfigResolver
|
|||||||
|
|
||||||
#[Autowire(param: 'notification.ntfy.dsn')]
|
#[Autowire(param: 'notification.ntfy.dsn')]
|
||||||
private ?string $notificationNtfyDsn = null,
|
private ?string $notificationNtfyDsn = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'sentry.dsn')]
|
||||||
|
private ?string $sentryDsn = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'sentry.environment')]
|
||||||
|
private ?string $sentryEnvironment = null,
|
||||||
|
|
||||||
|
#[Autowire(param: 'sentry.javascript_url')]
|
||||||
|
private ?string $sentryJavascriptUrl = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function validate(): bool
|
public function validate(): bool
|
||||||
@@ -120,4 +129,13 @@ final class ConfigResolver
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSentryConfig()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dsn' => $this->sentryDsn,
|
||||||
|
'environment' => $this->sentryEnvironment,
|
||||||
|
'javascript_url' => $this->sentryJavascriptUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/Base/Framework/Command/InitWorker.php
Normal file
90
src/Base/Framework/Command/InitWorker.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Base\Framework\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class InitWorker extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Environment $twig,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('init:worker')
|
||||||
|
->addOption('async', null, InputOption::VALUE_OPTIONAL, 'Run the async worker.',false)
|
||||||
|
->addOption('download', null, InputOption::VALUE_OPTIONAL, 'Run the download worker.', false)
|
||||||
|
->addOption('monitor', null, InputOption::VALUE_OPTIONAL, 'Run the monitor worker.', false)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$configFile = $this->twig->render("config/supervisord.conf.twig", []);
|
||||||
|
|
||||||
|
if ($this->optionExists($input, $output, 'async')) {
|
||||||
|
$configFile .= $this->twig->render("config/async.conf.twig", [
|
||||||
|
'replicas' => $this->getOptionValue('async', $input),
|
||||||
|
]) . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->optionExists($input, $output, 'download')) {
|
||||||
|
$configFile .= $this->twig->render("config/download.conf.twig", [
|
||||||
|
'replicas' => $this->getOptionValue('download', $input),
|
||||||
|
]). "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->optionExists($input, $output, 'monitor')) {
|
||||||
|
$configFile .= $this->twig->render("config/monitor.conf.twig", [
|
||||||
|
'replicas' => $this->getOptionValue('monitor', $input),
|
||||||
|
]). "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("" !== $configFile) {
|
||||||
|
$output->writeln("[init:worker] Writing /etc/supervisor/conf.d/supervisord.conf");
|
||||||
|
file_put_contents("/etc/supervisor/conf.d/supervisord.conf", $configFile);
|
||||||
|
|
||||||
|
$output->writeln("[init:worker] Starting supervisord");
|
||||||
|
shell_exec("/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf");
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln("[init:worker] No workers selected. Exiting.");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionExists(InputInterface $input, OutputInterface $output, string $option): bool
|
||||||
|
{
|
||||||
|
if ($input->getOption($option) !== false) {
|
||||||
|
$value = $input->getOption($option) ?? 1;
|
||||||
|
$output->writeln("[init:worker] transport: $option // $value // input var");;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optionKey = 'WORKER_' . strtoupper($option);
|
||||||
|
if (array_key_exists($optionKey, $_SERVER) && $_SERVER[$optionKey] !== null && $_SERVER[$optionKey] !== '') {
|
||||||
|
$output->writeln("[init:worker] transport: $option // $_SERVER[$optionKey] // env var");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOptionValue(string $option, InputInterface $input): int
|
||||||
|
{
|
||||||
|
if ($input->getOption($option) !== false) {
|
||||||
|
return $input->getOption($option) ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optionKey = 'WORKER_' . strtoupper($option);
|
||||||
|
return $_SERVER[$optionKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ final class IndexController extends AbstractController
|
|||||||
#[Route('/test')]
|
#[Route('/test')]
|
||||||
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
||||||
{
|
{
|
||||||
// $handler->handle(new MonitorTvShowCommand(82));
|
throw new \Exception('Test');
|
||||||
return $this->render('index/test.html.twig', []);
|
return $this->render('index/test.html.twig', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,37 @@ class MediaFiles
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $tvshowTitle
|
||||||
|
* @return array<SplFileInfo>|false
|
||||||
|
*/
|
||||||
|
public function tvshowExists(string $tvshowTitle): Map|false
|
||||||
|
{
|
||||||
|
$existingEpisodes = $this->getEpisodes($tvshowTitle, false);
|
||||||
|
|
||||||
|
if ($existingEpisodes->isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodes = new Map;
|
||||||
|
/** @var SplFileInfo $episode */
|
||||||
|
foreach ($existingEpisodes as $episode) {
|
||||||
|
$ptn = (object) (new PTN())->parse($episode->getFilename());
|
||||||
|
|
||||||
|
if (!property_exists($ptn, 'season') || !property_exists($ptn, 'episode')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodes->push($episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($episodes->count() > 0) {
|
||||||
|
return $episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function movieExists(string $title)
|
public function movieExists(string $title)
|
||||||
{
|
{
|
||||||
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class Paginator
|
|||||||
public function paginate($query, int $page = 1, int $limit = 5): Paginator
|
public function paginate($query, int $page = 1, int $limit = 5): Paginator
|
||||||
{
|
{
|
||||||
$paginator = new OrmPaginator($query);
|
$paginator = new OrmPaginator($query);
|
||||||
|
|
||||||
$paginator
|
$paginator
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->setFirstResult($limit * ($page - 1))
|
->setFirstResult($limit * ($page - 1))
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
$downloadCommands = [];
|
$downloadCommands = [];
|
||||||
foreach ($episodesInSeason as $episode) {
|
foreach ($episodesInSeason as $episode) {
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode['episode_number']);
|
$this->logger->info('> [DownloadTvSeasonHandler] ...Evaluating episode ' . $episode->episodeNumber);
|
||||||
|
|
||||||
$results = $this->getTvShowOptionsHandler->handle(
|
$results = $this->getTvShowOptionsHandler->handle(
|
||||||
new GetTvShowOptionsCommand(
|
new GetTvShowOptionsCommand(
|
||||||
$series->tmdbId,
|
$series->tmdbId,
|
||||||
$command->imdbId,
|
$command->imdbId,
|
||||||
$command->season,
|
$command->season,
|
||||||
$episode['episode_number']
|
$episode->episodeNumber
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
|
|
||||||
if (null !== $result) {
|
if (null !== $result) {
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Found 1 matching result');
|
||||||
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode['episode_number']);
|
$this->logger->info('> [DownloadTvSeasonHandler] ......Dispatching DownloadMediaCommand for "' . $series->title . '" season ' . $command->season . ' episode ' . $episode->episodeNumber);
|
||||||
$downloadCommand = new DownloadMediaCommand(
|
$downloadCommand = new DownloadMediaCommand(
|
||||||
$result->url,
|
$result->url,
|
||||||
$series->title,
|
$series->title,
|
||||||
@@ -99,10 +99,10 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
|||||||
->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->episodeNumber
|
||||||
&& null !== $episode->season
|
&& null !== $episode->season
|
||||||
)
|
)
|
||||||
->rekey(fn($episode) => $episode->episode);
|
->rekey(fn($episode) => $episode->episodeNumber);
|
||||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/Download/Action/Handler/MonitorHandlerTrait.php
Normal file
59
src/Download/Action/Handler/MonitorHandlerTrait.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Download\Action\Handler;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
trait MonitorHandlerTrait
|
||||||
|
{
|
||||||
|
private MonitorRepository $monitorRepository;
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
private TmdbClient $tmdb;
|
||||||
|
|
||||||
|
private function episodeReleasedAfterMonitorCreated(
|
||||||
|
string|DateTimeImmutable $monitorStartDate,
|
||||||
|
TmdbEpisodeDto $episodeInShow
|
||||||
|
): bool {
|
||||||
|
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
||||||
|
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
||||||
|
return $episodeAirDate >= $monitorStartDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
||||||
|
{
|
||||||
|
return $downloadedEpisodes->filter(
|
||||||
|
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||||
|
&& $episode->season === $episodeInShow->seasonNumber
|
||||||
|
)->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
|
||||||
|
{
|
||||||
|
return $this->monitorRepository->findOneBy([
|
||||||
|
'imdbId' => $monitor->getImdbId(),
|
||||||
|
'title' => $monitor->getTitle(),
|
||||||
|
'monitorType' => 'tvepisode',
|
||||||
|
'season' => $episode->seasonNumber,
|
||||||
|
'episode' => $episode->episodeNumber,
|
||||||
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
|
]) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refreshData(Monitor $monitor)
|
||||||
|
{
|
||||||
|
if (null === $monitor->getPoster()) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
||||||
|
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster;
|
||||||
|
if (null !== $poster && "" !== $poster) {
|
||||||
|
$monitor->setPoster($poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,13 @@ use App\EventLog\Framework\Repository\EventLogRepository;
|
|||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: EventLogRepository::class)]
|
#[ORM\Entity(repositoryClass: EventLogRepository::class)]
|
||||||
class EventLog
|
class EventLog
|
||||||
{
|
{
|
||||||
|
use TimestampableEntity;
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
|
|||||||
20
src/Library/Action/Command/GetMediaFromLibraryCommand.php
Normal file
20
src/Library/Action/Command/GetMediaFromLibraryCommand.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements CommandInterface<GetMediaFromLibraryCommand>
|
||||||
|
*/
|
||||||
|
class GetMediaFromLibraryCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $userId = null,
|
||||||
|
public ?string $mediaType = null,
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
public ?string $title = null,
|
||||||
|
public ?string $season = null,
|
||||||
|
public ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
169
src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php
Normal file
169
src/Library/Action/Handler/GetMediaInfoFromLibraryHandler.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Handler;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Base\Enum\MediaType;
|
||||||
|
use App\Base\Service\MediaFiles;
|
||||||
|
use App\Base\Util\PTN;
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use App\Library\Action\Result\GetMediaFromLibraryResult;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
|
use App\Tmdb\TmdbResult;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements HandlerInterface<GetMediaFromLibraryCommand,GetMediaFromLibraryResult>
|
||||||
|
*/
|
||||||
|
class GetMediaInfoFromLibraryHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TmdbClient $tmdb,
|
||||||
|
private readonly MediaFiles $mediaFiles,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly MonitorRepository $monitorRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(C $command): R
|
||||||
|
{
|
||||||
|
$result = new GetMediaFromLibraryResult();
|
||||||
|
$tmdbResult = $this->fetchTmdbData($command->imdbId, $command->mediaType);
|
||||||
|
|
||||||
|
if (null === $tmdbResult) {
|
||||||
|
$this->logger->warning('[GetMediaInfoFromLibraryHandler] TMDb result was not found, this may lead to issues in the rest of the library search', (array) $command);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setResultExists($tmdbResult->mediaType, $tmdbResult->title, $result);
|
||||||
|
if ($result->notExists()) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->parseFromTmdbResult($tmdbResult, $result);
|
||||||
|
|
||||||
|
if ($command->mediaType === MediaType::TvShow->value) {
|
||||||
|
$this->setEpisodes($tmdbResult, $result);
|
||||||
|
$this->setSeasons($tmdbResult, $result);
|
||||||
|
$this->setMonitors($command->userId, $command->imdbId, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchTmdbData(string $imdbId, string $mediaType): ?TmdbResult
|
||||||
|
{
|
||||||
|
return match($mediaType) {
|
||||||
|
MediaType::Movie->value => $this->tmdb->movieDetails($imdbId),
|
||||||
|
MediaType::TvShow->value => $this->tmdb->tvShowDetails($imdbId),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setResultExists(string $mediaType, string $title, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$fsResult = match($mediaType) {
|
||||||
|
MediaType::Movie->value => $this->mediaFiles->movieExists($title),
|
||||||
|
MediaType::TvShow->value => $this->mediaFiles->tvShowExists($title),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
if (false === $fsResult) {
|
||||||
|
$result->setExists(false);
|
||||||
|
} else {
|
||||||
|
$result->setExists(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseFromTmdbResult(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$result->setTitle($tmdbResult->title);
|
||||||
|
$result->setMediaType($tmdbResult->mediaType);
|
||||||
|
$result->setImdbId($tmdbResult->imdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEpisodes(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title);
|
||||||
|
|
||||||
|
$existingEpisodeMap = [];
|
||||||
|
foreach ($existingEpisodeFiles as $file) {
|
||||||
|
/** @var \SplFileInfo $file */
|
||||||
|
$ptn = (object) new PTN()->parse($file->getBasename());
|
||||||
|
|
||||||
|
if (!array_key_exists($ptn->season, $existingEpisodeMap)) {
|
||||||
|
$existingEpisodeMap[$ptn->season] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($ptn->episode, $existingEpisodeMap[$ptn->season])) {
|
||||||
|
$existingEpisodeMap[$ptn->season][] = $ptn->episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingEpisodes = [];
|
||||||
|
$missingEpisodes = [];
|
||||||
|
foreach ($tmdbResult->episodes as $season => $episodes) {
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
if (array_key_exists($season, $existingEpisodeMap)) {
|
||||||
|
if (in_array($episode->episodeNumber, $existingEpisodeMap[$season])) {
|
||||||
|
$existingEpisodes[] = $episode;
|
||||||
|
} else {
|
||||||
|
$missingEpisodes[] = $episode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$missingEpisodes[] = $episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result->setEpisodes($existingEpisodes);
|
||||||
|
$result->setMissingEpisodes($missingEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSeasons(TmdbResult $tmdbResult, GetMediaFromLibraryResult $result): void
|
||||||
|
{
|
||||||
|
$existingEpisodeFiles = $this->mediaFiles->tvshowExists($tmdbResult->title);
|
||||||
|
|
||||||
|
$existingEpisodeMap = [];
|
||||||
|
foreach ($existingEpisodeFiles as $file) {
|
||||||
|
/** @var \SplFileInfo $file */
|
||||||
|
$ptn = (object) new PTN()->parse($file->getBasename());
|
||||||
|
$existingEpisodeMap[$ptn->season][] = $ptn->episode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingFullSeasons = [];
|
||||||
|
$existingPartialSeasons = [];
|
||||||
|
$missingSeasons = [];
|
||||||
|
foreach ($existingEpisodeMap as $season => $episodes) {
|
||||||
|
if (count($tmdbResult->episodes[$season]) === count($episodes)) {
|
||||||
|
$existingFullSeasons[] = $season;
|
||||||
|
} elseif (count($episodes) > 0) {
|
||||||
|
$existingPartialSeasons[] = $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$seasons = array_keys($tmdbResult->episodes);
|
||||||
|
foreach ($seasons as $season) {
|
||||||
|
if (!in_array($season, $existingFullSeasons) && !in_array($season, $existingPartialSeasons)) {
|
||||||
|
$missingSeasons[] = $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->setSeasons($existingFullSeasons);
|
||||||
|
$result->setPartialSeasons($existingPartialSeasons);
|
||||||
|
$result->setMissingSeasons($missingSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMonitors(int $userId, string $imdbId, GetMediaFromLibraryResult $result)
|
||||||
|
{
|
||||||
|
$result->setMonitorCount(
|
||||||
|
$this->monitorRepository->countUserChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
$result->setActiveMonitorCount(
|
||||||
|
$this->monitorRepository->countUserActiveChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
$result->setCompleteMonitorCount(
|
||||||
|
$this->monitorRepository->countUserCompleteChildrenByParentId($userId, $imdbId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Library/Action/Input/GetMediaInfoFromLibraryInput.php
Normal file
39
src/Library/Action/Input/GetMediaInfoFromLibraryInput.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Input;
|
||||||
|
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements InputInterface<GetMediaInfoFromLibraryInput, GetMediaFromLibraryCommand>
|
||||||
|
*/
|
||||||
|
class GetMediaInfoFromLibraryInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceRequest('imdbId', nullify: true)]
|
||||||
|
public ?string $imdbId = null,
|
||||||
|
#[SourceRequest('title', nullify: true)]
|
||||||
|
public ?string $title = null,
|
||||||
|
#[SourceRequest('season', nullify: true)]
|
||||||
|
public ?string $season = null,
|
||||||
|
#[SourceRequest('episode', nullify: true)]
|
||||||
|
public ?string $episode = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): C
|
||||||
|
{
|
||||||
|
if (null === $this->imdbId && null === $this->title) {
|
||||||
|
throw new \InvalidArgumentException('Either imdbId or title must be set', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetMediaFromLibraryCommand(
|
||||||
|
imdbId: $this->imdbId,
|
||||||
|
title: $this->title,
|
||||||
|
season: $this->season,
|
||||||
|
episode: $this->episode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Library/Action/Result/GetMediaFromLibraryResult.php
Normal file
171
src/Library/Action/Result/GetMediaFromLibraryResult.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Library\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class GetMediaFromLibraryResult implements ResultInterface
|
||||||
|
{
|
||||||
|
private bool $exists;
|
||||||
|
private ?string $title = null;
|
||||||
|
private ?string $imdbId = null;
|
||||||
|
private ?string $mediaType = null;
|
||||||
|
private ?array $episodes = null;
|
||||||
|
private ?array $missingEpisodes = null;
|
||||||
|
private ?array $seasons = null;
|
||||||
|
private ?array $partialSeasons = null;
|
||||||
|
private ?array $missingSeasons = null;
|
||||||
|
private ?int $monitorCount = null; // Monitor Repo
|
||||||
|
private ?int $activeMonitorCount = null; // Monitor Repo
|
||||||
|
private ?int $completeMonitorCount = null; // Monitor Repo
|
||||||
|
|
||||||
|
public function exists(): bool
|
||||||
|
{
|
||||||
|
return $this->exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notExists(): bool
|
||||||
|
{
|
||||||
|
return !$this->exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExists(bool $exists): void
|
||||||
|
{
|
||||||
|
$this->exists = $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(?string $title): void
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImdbId(): ?string
|
||||||
|
{
|
||||||
|
return $this->imdbId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImdbId(?string $imdbId): void
|
||||||
|
{
|
||||||
|
$this->imdbId = $imdbId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMediaType(): ?string
|
||||||
|
{
|
||||||
|
return $this->mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMediaType(?string $mediaType): void
|
||||||
|
{
|
||||||
|
$this->mediaType = $mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodes(): ?array
|
||||||
|
{
|
||||||
|
return $this->episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEpisodes(?array $episodes): void
|
||||||
|
{
|
||||||
|
$this->episodes = $episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodeCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->episodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingEpisodes(): ?array
|
||||||
|
{
|
||||||
|
return $this->missingEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMissingEpisodes(?array $missingEpisodes): void
|
||||||
|
{
|
||||||
|
$this->missingEpisodes = $missingEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingEpisodeCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->missingEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSeasons(?array $seasons): void
|
||||||
|
{
|
||||||
|
$this->seasons = $seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->seasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPartialSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->partialSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPartialSeasons(?array $partialSeasons): void
|
||||||
|
{
|
||||||
|
$this->partialSeasons = $partialSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPartialSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->partialSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingSeasons(): ?array
|
||||||
|
{
|
||||||
|
return $this->missingSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMissingSeasons(?array $missingSeasons): void
|
||||||
|
{
|
||||||
|
$this->missingSeasons = $missingSeasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingSeasonCount(): ?int
|
||||||
|
{
|
||||||
|
return count($this->missingSeasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->monitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMonitorCount(?int $monitorCount): void
|
||||||
|
{
|
||||||
|
$this->monitorCount = $monitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->activeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActiveMonitorCount(?int $activeMonitorCount): void
|
||||||
|
{
|
||||||
|
$this->activeMonitorCount = $activeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCompleteMonitorCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->completeMonitorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCompleteMonitorCount(?int $completeMonitorCount): void
|
||||||
|
{
|
||||||
|
$this->completeMonitorCount = $completeMonitorCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Monitor\Action\Result\AddMonitorResult;
|
|||||||
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\Monitor\MonitorEvents;
|
use App\Monitor\MonitorEvents;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
@@ -22,13 +23,17 @@ readonly class AddMonitorHandler implements HandlerInterface
|
|||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private MonitorRepository $movieMonitorRepository,
|
private MonitorRepository $movieMonitorRepository,
|
||||||
private UserRepository $userRepository,
|
private UserRepository $userRepository,
|
||||||
|
private TmdbClient $tmdb,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$user = $this->userRepository->find($command->userId);
|
$user = $this->userRepository->find($command->userId);
|
||||||
|
$poster = $this->getPoster($command->imdbId);
|
||||||
|
|
||||||
$monitor = (new Monitor())
|
$monitor = (new Monitor())
|
||||||
->setUser($user)
|
->setUser($user)
|
||||||
|
->setPoster($poster)
|
||||||
->setTmdbId($command->tmdbId)
|
->setTmdbId($command->tmdbId)
|
||||||
->setImdbId($command->imdbId)
|
->setImdbId($command->imdbId)
|
||||||
->setTitle($command->title)
|
->setTitle($command->title)
|
||||||
@@ -56,4 +61,10 @@ readonly class AddMonitorHandler implements HandlerInterface
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getPoster(string $imdbId): ?string
|
||||||
|
{
|
||||||
|
$data = $this->tmdb->tvShowDetails($imdbId);
|
||||||
|
return $data->poster;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Download\Framework\Repository\DownloadRepository;
|
|||||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\Monitor\MonitorEvents;
|
use App\Monitor\MonitorEvents;
|
||||||
use App\Tmdb\TmdbClient;
|
use App\Tmdb\TmdbClient;
|
||||||
@@ -43,6 +44,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
try {
|
try {
|
||||||
$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());
|
||||||
|
$this->refreshData($monitor);
|
||||||
|
|
||||||
$this->bus->dispatch(new AddEventLogCommand(
|
$this->bus->dispatch(new AddEventLogCommand(
|
||||||
$monitor->getUser(),
|
$monitor->getUser(),
|
||||||
@@ -151,4 +153,15 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function refreshData(Monitor $monitor)
|
||||||
|
{
|
||||||
|
if (null === $monitor->getPoster()) {
|
||||||
|
$this->logger->info('> [MonitorTvEpisodeHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
||||||
|
$poster = $monitor->getParent()->getPoster();
|
||||||
|
if (null !== $poster && "" !== $poster) {
|
||||||
|
$monitor->setPoster($poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
||||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||||
|
$this->refreshData($monitor);
|
||||||
|
|
||||||
// Check current episodes
|
// Check current episodes
|
||||||
$downloadedEpisodes = $this->mediaFiles
|
$downloadedEpisodes = $this->mediaFiles
|
||||||
@@ -157,4 +158,15 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
|||||||
'status' => ['New', 'Active', 'In Progress']
|
'status' => ['New', 'Active', 'In Progress']
|
||||||
]) !== null;
|
]) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function refreshData(Monitor $monitor)
|
||||||
|
{
|
||||||
|
if (null === $monitor->getPoster()) {
|
||||||
|
$this->logger->info('> [MonitorTvShowHandler] Refreshing poster for "' . $monitor->getTitle() . '"');
|
||||||
|
$poster = $this->tmdb->tvshowDetails($monitor->getImdbId())->poster;
|
||||||
|
if (null !== $poster && "" !== $poster) {
|
||||||
|
$monitor->setPoster($poster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class ApiController extends AbstractController
|
|||||||
'allDay' => true,
|
'allDay' => true,
|
||||||
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
||||||
'borderColor' => $eventColors[$monitor->getImdbId()],
|
'borderColor' => $eventColors[$monitor->getImdbId()],
|
||||||
|
'attachment' => $monitor->getPoster(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Monitor\Framework\Controller;
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use Spatie\IcalendarGenerator\Components\Calendar;
|
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||||
@@ -27,10 +28,14 @@ class CalendarController extends AbstractController
|
|||||||
->refreshInterval(10);
|
->refreshInterval(10);
|
||||||
|
|
||||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
$calendar->event(Map::from($monitors)->map(function ($monitor) {
|
$calendar->event(Map::from($monitors)->map(function (Monitor $monitor) {
|
||||||
return new Event($monitor->getTitle())
|
$event = new Event($monitor->getTitle())
|
||||||
->startsAt($monitor->getAirDate())
|
->startsAt($monitor->getAirDate())
|
||||||
->fullDay();
|
->fullDay();
|
||||||
|
if (null !== $monitor->getPoster()) {
|
||||||
|
$event->attachment($monitor->getPoster());
|
||||||
|
}
|
||||||
|
return $event;
|
||||||
})->toArray());
|
})->toArray());
|
||||||
|
|
||||||
return new Response($calendar->get(), 200, [
|
return new Response($calendar->get(), 200, [
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
namespace App\Monitor\Framework\Controller;
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
use App\Download\Action\Input\DeleteDownloadInput;
|
use App\Download\Action\Input\DeleteDownloadInput;
|
||||||
|
use App\Library\Action\Command\GetMediaFromLibraryCommand;
|
||||||
|
use App\Library\Action\Handler\GetMediaInfoFromLibraryHandler;
|
||||||
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\Entity\Monitor;
|
||||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||||
|
use App\Search\Action\Handler\GetMediaInfoHandler;
|
||||||
|
use App\Tmdb\TmdbClient;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
@@ -23,7 +29,7 @@ class WebController extends AbstractController
|
|||||||
private readonly Environment $renderer,
|
private readonly Environment $renderer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/monitors', name: 'app_monitors', methods: ['GET'])]
|
#[Route('/monitors', name: 'app.monitors', methods: ['GET'])]
|
||||||
public function addMonitor()
|
public function addMonitor()
|
||||||
{
|
{
|
||||||
return $this->render('monitor/index.html.twig');
|
return $this->render('monitor/index.html.twig');
|
||||||
@@ -34,4 +40,29 @@ class WebController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('monitor/upcoming-episodes.html.twig');
|
return $this->render('monitor/upcoming-episodes.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/monitors/{id}', name: 'app.monitor.view', methods: ['GET'])]
|
||||||
|
public function viewMonitor(Monitor $monitor, GetMediaInfoHandler $getMediaInfoHandler, GetMediaInfoFromLibraryHandler $handler)
|
||||||
|
{
|
||||||
|
$media = $getMediaInfoHandler->handle(
|
||||||
|
new GetMediaInfoCommand(
|
||||||
|
imdbId: $monitor->getImdbId(),
|
||||||
|
mediaType: 'tvshows',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$libraryResult = $handler->handle(
|
||||||
|
new GetMediaFromLibraryCommand(
|
||||||
|
$this->getUser()->getId(),
|
||||||
|
$media->media->mediaType,
|
||||||
|
$media->media->imdbId,
|
||||||
|
$media->media->title,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render('monitor/view.html.twig', [
|
||||||
|
'monitor' => $monitor,
|
||||||
|
'results' => $media,
|
||||||
|
'library' => $libraryResult
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class Monitor
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?int $searchCount = null;
|
private ?int $searchCount = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?string $poster = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private bool $onlyFuture = true;
|
private bool $onlyFuture = true;
|
||||||
|
|
||||||
@@ -230,6 +233,17 @@ class Monitor
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPoster(): ?string
|
||||||
|
{
|
||||||
|
return $this->poster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPoster(?string $poster): ?self
|
||||||
|
{
|
||||||
|
$this->poster = $poster;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getParent(): ?self
|
public function getParent(): ?self
|
||||||
{
|
{
|
||||||
return $this->parent;
|
return $this->parent;
|
||||||
|
|||||||
@@ -41,4 +41,83 @@ class MonitorRepository extends ServiceEntityRepository
|
|||||||
->getQuery();
|
->getQuery();
|
||||||
return $query->getResult();
|
return $query->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActiveUserMonitors()
|
||||||
|
{
|
||||||
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
|
->andWhere('m.parent IS NULL')
|
||||||
|
->setParameter('statuses', ['New', 'In Progress', 'Active'])
|
||||||
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChildMonitorsByParentId(int $parentId)
|
||||||
|
{
|
||||||
|
return $this->asPaginator(
|
||||||
|
$this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere("m.parent = :parentId")
|
||||||
|
->setParameter('parentId', $parentId)
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCompleteUserMonitors()
|
||||||
|
{
|
||||||
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere('m.status = :status')
|
||||||
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
|
->setParameter('status', 'Complete')
|
||||||
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserActiveChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('statuses', ['Active', 'New', 'In Progress'])
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserCompleteChildrenByParentId(int $userId, string $imdbId): ?int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('m')
|
||||||
|
->select('COUNT(m.id)')
|
||||||
|
->andWhere('m.user = :user')
|
||||||
|
->andWhere('m.imdbId = :imdbId')
|
||||||
|
->andWhere('m.status IN (:statuses)')
|
||||||
|
->setParameter('user', $userId)
|
||||||
|
->setParameter('statuses', ['Complete'])
|
||||||
|
->setParameter('imdbId', $imdbId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Tmdb\Event\Listener\Request\ApiTokenRequestListener;
|
|||||||
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
||||||
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
||||||
use Tmdb\Event\RequestEvent;
|
use Tmdb\Event\RequestEvent;
|
||||||
|
use Tmdb\Repository\DiscoverRepository;
|
||||||
use Tmdb\Repository\MovieRepository;
|
use Tmdb\Repository\MovieRepository;
|
||||||
use Tmdb\Repository\SearchRepository;
|
use Tmdb\Repository\SearchRepository;
|
||||||
use Tmdb\Repository\TvEpisodeRepository;
|
use Tmdb\Repository\TvEpisodeRepository;
|
||||||
@@ -30,6 +31,7 @@ use Tmdb\Token\Api\BearerToken;
|
|||||||
class TmdbClient
|
class TmdbClient
|
||||||
{
|
{
|
||||||
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
const POSTER_IMG_PATH = "https://image.tmdb.org/t/p/w500";
|
||||||
|
const APPEND_TO_RESPONSE = 'external_ids,credits,watch/providers';
|
||||||
|
|
||||||
protected Client $client;
|
protected Client $client;
|
||||||
protected MovieRepository $movieRepository;
|
protected MovieRepository $movieRepository;
|
||||||
@@ -38,6 +40,8 @@ class TmdbClient
|
|||||||
protected TvEpisodeRepository $tvEpisodeRepository;
|
protected TvEpisodeRepository $tvEpisodeRepository;
|
||||||
protected SearchRepository $searchRepository;
|
protected SearchRepository $searchRepository;
|
||||||
|
|
||||||
|
protected DiscoverRepository $discoverRepository;
|
||||||
|
|
||||||
protected array $mediaTypeMap = [
|
protected array $mediaTypeMap = [
|
||||||
MediaType::Movie->value => MediaType::Movie->value,
|
MediaType::Movie->value => MediaType::Movie->value,
|
||||||
MediaType::TvShow->value => MediaType::TvShow->value,
|
MediaType::TvShow->value => MediaType::TvShow->value,
|
||||||
@@ -48,11 +52,14 @@ class TmdbClient
|
|||||||
|
|
||||||
protected $repos = [];
|
protected $repos = [];
|
||||||
|
|
||||||
|
private ?string $originalLanguage = 'en';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
|
#[Autowire(env: 'TMDB_ORIGINAL_LANGUAGE')] ?string $originalLanguage = null,
|
||||||
) {
|
) {
|
||||||
$this->client = new Client(
|
$this->client = new Client(
|
||||||
[
|
[
|
||||||
@@ -74,7 +81,7 @@ class TmdbClient
|
|||||||
'hydration' => [
|
'hydration' => [
|
||||||
'event_listener_handles_hydration' => false,
|
'event_listener_handles_hydration' => false,
|
||||||
'only_for_specified_models' => []
|
'only_for_specified_models' => []
|
||||||
]
|
],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,11 +114,14 @@ class TmdbClient
|
|||||||
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
|
$this->tvSeasonRepository = new TvSeasonRepository($this->client);
|
||||||
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
|
$this->tvEpisodeRepository = new TvEpisodeRepository($this->client);
|
||||||
$this->searchRepository = new SearchRepository($this->client);
|
$this->searchRepository = new SearchRepository($this->client);
|
||||||
|
$this->discoverRepository = new DiscoverRepository($this->client);
|
||||||
|
|
||||||
$this->repos = [
|
$this->repos = [
|
||||||
MediaType::Movie->value => $this->movieRepository,
|
MediaType::Movie->value => $this->movieRepository,
|
||||||
MediaType::TvShow->value => $this->tvRepository,
|
MediaType::TvShow->value => $this->tvRepository,
|
||||||
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
|
MediaType::TvEpisode->value => $this->tvEpisodeRepository,
|
||||||
];
|
];
|
||||||
|
$this->originalLanguage = $originalLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function search(string $term): TmdbResult|Map
|
public function search(string $term): TmdbResult|Map
|
||||||
@@ -133,7 +143,7 @@ class TmdbClient
|
|||||||
{
|
{
|
||||||
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => 'external_ids,credits']),
|
$this->movieRepository->getApi()->getMovie($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]),
|
||||||
MediaType::Movie->value,
|
MediaType::Movie->value,
|
||||||
$imdbId
|
$imdbId
|
||||||
);
|
);
|
||||||
@@ -142,7 +152,7 @@ class TmdbClient
|
|||||||
public function tvshowDetails(string $imdbId): ?TmdbResult
|
public function tvshowDetails(string $imdbId): ?TmdbResult
|
||||||
{
|
{
|
||||||
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
$tmdbId = $this->findByImdbId($imdbId)['id'];
|
||||||
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => 'external_ids,credits']);
|
$media = $this->tvRepository->getApi()->getTvShow($tmdbId, ['append_to_response' => static::APPEND_TO_RESPONSE]);
|
||||||
|
|
||||||
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
|
$media['seasons'] = Map::from($media['seasons'])->filter(function ($data) {
|
||||||
return $data['season_number'] !== 0 &&
|
return $data['season_number'] !== 0 &&
|
||||||
@@ -163,7 +173,7 @@ class TmdbClient
|
|||||||
|
|
||||||
public function tvSeasonDetails(string $tmdbId, int $season): array
|
public function tvSeasonDetails(string $tmdbId, int $season): array
|
||||||
{
|
{
|
||||||
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => 'external_ids,credits']);
|
$result = $this->tvSeasonRepository->getApi()->getSeason($tmdbId, $season, ['append_to_response' => static::APPEND_TO_RESPONSE]);
|
||||||
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
|
$result['episodes'] = Map::from($result['episodes'])->map(function ($data) {
|
||||||
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
|
$data['still_path'] = self::POSTER_IMG_PATH . $data['still_path'];
|
||||||
$data['poster'] = $data['still_path'];
|
$data['poster'] = $data['still_path'];
|
||||||
@@ -174,7 +184,7 @@ class TmdbClient
|
|||||||
|
|
||||||
public function tvEpisodeDetails(string $tmdbId, string $showImdbId, 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' => static::APPEND_TO_RESPONSE]);
|
||||||
return $this->parseResult(
|
return $this->parseResult(
|
||||||
$result,
|
$result,
|
||||||
MediaType::TvEpisode->value,
|
MediaType::TvEpisode->value,
|
||||||
@@ -193,7 +203,10 @@ class TmdbClient
|
|||||||
|
|
||||||
public function popularMovies(int $resultCount = 6): Map
|
public function popularMovies(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
$results = $this->movieRepository->getApi()->getPopular();
|
$results = $this->discoverRepository->getApi()->discoverMovies([
|
||||||
|
'with_original_language' => $this->originalLanguage,
|
||||||
|
'append_to_response' => 'external_ids,watch/providers',
|
||||||
|
]);
|
||||||
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
$result['media_type'] = MediaType::Movie->value;
|
$result['media_type'] = MediaType::Movie->value;
|
||||||
return $result;
|
return $result;
|
||||||
@@ -206,7 +219,10 @@ class TmdbClient
|
|||||||
|
|
||||||
public function popularTvShows(int $resultCount = 6): Map
|
public function popularTvShows(int $resultCount = 6): Map
|
||||||
{
|
{
|
||||||
$results = $this->tvRepository->getApi()->getPopular();
|
$results = $this->discoverRepository->getApi()->discoverTv([
|
||||||
|
'with_original_language' => $this->originalLanguage,
|
||||||
|
'append_to_response' => 'external_ids,watch/providers',
|
||||||
|
]);
|
||||||
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
$results['results'] = Map::from($results['results'])->map(function ($result) {
|
||||||
$result['media_type'] = MediaType::TvShow->value;
|
$result['media_type'] = MediaType::TvShow->value;
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -68,4 +68,10 @@ class Torrentio
|
|||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDestinationUrl(string $url)
|
||||||
|
{
|
||||||
|
$request = get_headers($url)[8];
|
||||||
|
return explode(' ', $request)[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
|||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||||
|
use App\Torrentio\Client\Torrentio;
|
||||||
use App\Torrentio\Exception\TorrentioRateLimitException;
|
use App\Torrentio\Exception\TorrentioRateLimitException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
@@ -21,6 +22,8 @@ use Symfony\UX\Turbo\TurboBundle;
|
|||||||
|
|
||||||
final class WebController extends AbstractController
|
final class WebController extends AbstractController
|
||||||
{
|
{
|
||||||
|
const REAL_DEBRID_STREAM_URL = 'https://real-debrid.com/streaming-%s';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||||
@@ -99,4 +102,14 @@ final class WebController extends AbstractController
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/torrentio/stream/{url}', name: 'app.torrentio.stream')]
|
||||||
|
public function streamVideo(string $url, Torrentio $torrentio): Response
|
||||||
|
{
|
||||||
|
$destinationUrl = $torrentio->getDestinationUrl(\base64_decode($url));
|
||||||
|
$urlPathParts = explode('/', parse_url($destinationUrl)['path']);
|
||||||
|
$videoId = $urlPathParts[2];
|
||||||
|
$url = sprintf(self::REAL_DEBRID_STREAM_URL, $videoId);
|
||||||
|
return $this->redirect($url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ final class MonitorList extends AbstractController
|
|||||||
|
|
||||||
use PaginateTrait;
|
use PaginateTrait;
|
||||||
|
|
||||||
|
#[LiveProp(writable: true)]
|
||||||
|
public ?int $parentMonitorId = null;
|
||||||
|
|
||||||
#[LiveProp(writable: true)]
|
#[LiveProp(writable: true)]
|
||||||
public string $term = "";
|
public string $term = "";
|
||||||
|
|
||||||
#[LiveProp(writable: true)]
|
#[LiveProp(writable: true)]
|
||||||
public string $type;
|
public string $type = "";
|
||||||
|
|
||||||
#[LiveProp(writable: true)]
|
#[LiveProp(writable: true)]
|
||||||
public bool $isWidget = true;
|
public bool $isWidget = true;
|
||||||
@@ -33,7 +36,9 @@ final class MonitorList extends AbstractController
|
|||||||
#[LiveAction]
|
#[LiveAction]
|
||||||
public function getMonitors()
|
public function getMonitors()
|
||||||
{
|
{
|
||||||
if ($this->type === "active") {
|
if (null !== $this->parentMonitorId) {
|
||||||
|
return $this->getChildMonitorsByParentId($this->parentMonitorId);
|
||||||
|
} elseif ($this->type === "active") {
|
||||||
return $this->getActiveUserMonitors();
|
return $this->getActiveUserMonitors();
|
||||||
} elseif ($this->type === "complete") {
|
} elseif ($this->type === "complete") {
|
||||||
return $this->getCompleteUserMonitors();
|
return $this->getCompleteUserMonitors();
|
||||||
@@ -48,6 +53,7 @@ final class MonitorList extends AbstractController
|
|||||||
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
return $this->asPaginator($this->monitorRepository->createQueryBuilder('m')
|
||||||
->andWhere('m.status IN (:statuses)')
|
->andWhere('m.status IN (:statuses)')
|
||||||
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
->andWhere('(m.title LIKE :term OR m.imdbId LIKE :term OR m.monitorType LIKE :term OR m.status LIKE :term)')
|
||||||
|
->andWhere('m.parent IS NULL')
|
||||||
->setParameter('statuses', ['New', 'In Progress', 'Active'])
|
->setParameter('statuses', ['New', 'In Progress', 'Active'])
|
||||||
->setParameter('term', '%'.$this->term.'%')
|
->setParameter('term', '%'.$this->term.'%')
|
||||||
->orderBy('m.id', 'DESC')
|
->orderBy('m.id', 'DESC')
|
||||||
@@ -67,4 +73,16 @@ final class MonitorList extends AbstractController
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[LiveAction]
|
||||||
|
public function getChildMonitorsByParentId(int $parentId)
|
||||||
|
{
|
||||||
|
return $this->asPaginator(
|
||||||
|
$this->monitorRepository->createQueryBuilder('m')
|
||||||
|
->andWhere("m.parent = :parentId")
|
||||||
|
->setParameter('parentId', $parentId)
|
||||||
|
->orderBy('m.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,4 +112,18 @@ class UtilExtension
|
|||||||
|
|
||||||
return new EpisodeIdDto($season, $episode);
|
return new EpisodeIdDto($season, $episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[AsTwigFunction('sentry_enabled')]
|
||||||
|
public function sentryEnabled(): bool
|
||||||
|
{
|
||||||
|
$sentryConfig = $this->config->getSentryConfig();
|
||||||
|
return $sentryConfig['javascript_url'] !== null &&
|
||||||
|
$sentryConfig['javascript_url'] !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsTwigFilter('base64_encode')]
|
||||||
|
public function base64_encode(string $data): string
|
||||||
|
{
|
||||||
|
return \base64_encode($data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
symfony.lock
24
symfony.lock
@@ -101,6 +101,18 @@
|
|||||||
"bin/phpunit"
|
"bin/phpunit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sentry/sentry-symfony": {
|
||||||
|
"version": "5.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.0",
|
||||||
|
"ref": "b6cb4b34429dadecd7187852123be19d628fa37a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/sentry.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"spomky-labs/pwa-bundle": {
|
"spomky-labs/pwa-bundle": {
|
||||||
"version": "1.2.5"
|
"version": "1.2.5"
|
||||||
},
|
},
|
||||||
@@ -232,6 +244,18 @@
|
|||||||
"config/packages/messenger.yaml"
|
"config/packages/messenger.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/monolog-bundle": {
|
||||||
|
"version": "3.10",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.7",
|
||||||
|
"ref": "f5f5f3e4c23f5349796b7de587f19c51e7104299"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/monolog.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/notifier": {
|
"symfony/notifier": {
|
||||||
"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://bugs.caldwell.digital/js-sdk-loader/8dddf7fb26fbec602ad212173a942450.min.js" crossorigin="anonymous"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cyan-950 flex flex-col h-full">
|
<body class="bg-cyan-950 flex flex-col h-full">
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||||
|
|
||||||
|
{% if sentry_enabled() %}
|
||||||
|
<script src="{{ sentry_javascript_url }}" crossorigin="anonymous"></script>
|
||||||
|
{% endif %}
|
||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@@ -22,9 +26,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
<div class="col-span-6 md:col-span-5 h-screen overflow-y-scroll">
|
||||||
<twig:Header />
|
<twig:Header />
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex flex-col md:flex-row justify-between md:items-center">
|
||||||
<h2 class="px-4 mt-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
<h2 class="px-4 mt-4 mb-4 md:mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||||
<div class="flex mt-4 gap-2 items-center grow-0 md:px-4">
|
<div class="flex mx-4 mb-2 md:mt-4 gap-2 items-center grow-0 md:px-4">
|
||||||
{% block action_buttons %}{% endblock %}
|
{% block action_buttons %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
{% extends 'bare.html.twig' %}
|
{% extends 'bare.html.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2 class="px-4 py-4 text-3xl font-extrabold text-orange-500">{{ }}</h2>
|
<h2 class="px-4 py-4 text-3xl font-extrabold text-orange-500">500</h2>
|
||||||
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
|
<div class="flex flex-col bg-orange-500/50 p-4 rounded-lg gap-4 w-full md:w-[420px] border-orange-500 border-2 text-gray-50 animate-fade">
|
||||||
<div class="flex flex-col m-0 text-center">
|
<div class="flex flex-col m-0 text-center">
|
||||||
<h3 class="text-2xl text-bold text-center text-gray-50">Oh crap!</h3>
|
<h3 class="text-2xl text-bold text-center text-gray-50">Oh crap!</h3>
|
||||||
<small>(or is it?)</small>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-2">There are many things I'm capable of, but this ain't one of 'em!</p>
|
<p class="mb-2">There are many things I'm capable of, but this ain't one of 'em!</p>
|
||||||
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto">
|
<pre class="bg-gray-800 text-white p-4 rounded-md overflow-x-auto">
|
||||||
|
|||||||
@@ -17,10 +17,17 @@
|
|||||||
class="px-6 py-3 text-start">
|
class="px-6 py-3 text-start">
|
||||||
ID
|
ID
|
||||||
</th>
|
</th>
|
||||||
|
{% if null != parentMonitorId %}
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="hidden md:table-cell px-6 py-3 text-start">
|
class="hidden md:table-cell px-6 py-3 text-start">
|
||||||
Search Count
|
Search Count
|
||||||
</th>
|
</th>
|
||||||
|
{% else %}
|
||||||
|
<th scope="col"
|
||||||
|
class="hidden md:table-cell px-6 py-3 text-start">
|
||||||
|
Episodes
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="hidden md:table-cell px-6 py-3 text-start">
|
class="hidden md:table-cell px-6 py-3 text-start">
|
||||||
Created at
|
Created at
|
||||||
@@ -48,7 +55,7 @@
|
|||||||
{% 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-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
||||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
<a href="{{ path('app.monitors') }}">View All Monitors</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<tr{{ attributes }} is="monitor-list-row" id="monitor_{{ monitor.id }}" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900"
|
<tr{{ attributes }} is="monitor-list-row" id="monitor_{{ monitor.id }}" class="dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer"
|
||||||
monitor-id="{{ monitor.id }}"
|
monitor-id="{{ monitor.id }}"
|
||||||
|
parent-id="{{ monitor.parent.id ?? null }}"
|
||||||
imdb-id="{{ monitor.imdbId }}"
|
imdb-id="{{ monitor.imdbId }}"
|
||||||
media-title="{{ monitor.title }}"
|
media-title="{{ monitor.title }}"
|
||||||
season="{{ monitor.season }}"
|
season="{{ monitor.season }}"
|
||||||
episode="{{ monitor.episode }}"
|
episode="{{ monitor.episode ?? '-' }}"
|
||||||
status="{{ monitor.status }}"
|
status="{{ monitor.status }}"
|
||||||
search-count="{{ monitor.searchCount }}"
|
search-count="{{ monitor.searchCount }}"
|
||||||
media-type="{{ monitor.monitorType|monitor_type }}"
|
media-type="{{ monitor.monitorType|monitor_type }}"
|
||||||
episode-id="{{ monitor|monitor_media_id }}"
|
episode-id="{{ monitor|monitor_media_id }}"
|
||||||
created-at="{{ monitor.createdAt|date('m/d/Y g:i a') }}"
|
created-at="{{ monitor.createdAt|date('m/d/Y g:i a') }}"
|
||||||
last-search="{{ monitor.lastSearch|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') }}"
|
downloaded-at="{{null != monitor.downloadedAt ? monitor.downloadedAt|date('m/d/Y g:i a') : '-' }}"
|
||||||
|
air-date="{{ null != monitor.airDate ? monitor.airDate|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}) }}"
|
||||||
@@ -26,9 +28,17 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
{{ monitor|monitor_media_id }}
|
{{ monitor|monitor_media_id }}
|
||||||
</td>
|
</td>
|
||||||
|
{# Monitor is a CHILD monitor #}
|
||||||
|
{% if null != monitor.parent %}
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
||||||
{{ monitor.searchCount }}
|
{{ monitor.searchCount }}
|
||||||
</td>
|
</td>
|
||||||
|
{% else %}
|
||||||
|
{# Monitor is a PARENT monitor #}
|
||||||
|
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
{{ monitor.children|length }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td class="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm">
|
<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>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app_monitors') }}"
|
<a href="{{ path('app.monitors') }}"
|
||||||
class="block rounded-lg
|
class="block rounded-lg
|
||||||
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
||||||
px-4 py-2 text-sm font-medium text-gray-50">
|
px-4 py-2 text-sm font-medium text-gray-50">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<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">
|
<dialog{{ attributes }} is="preview-content-dialog" class="py-3 px-4 w-full md:w-[{{ mdWidth|default('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>
|
||||||
|
|||||||
10
templates/config/async.conf.twig
Normal file
10
templates/config/async.conf.twig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-worker]
|
||||||
|
command=/app/bin/console messenger:consume async -vv --time-limit 3600
|
||||||
|
numprocs={{ replicas|default(1) }}
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
10
templates/config/download.conf.twig
Normal file
10
templates/config/download.conf.twig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-downloader]
|
||||||
|
command=/app/bin/console messenger:consume download -vv --time-limit 3600
|
||||||
|
numprocs={{ replicas|default(1) }}
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
10
templates/config/monitor.conf.twig
Normal file
10
templates/config/monitor.conf.twig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[program:torsearch-scheduler]
|
||||||
|
command=/app/bin/console messenger:consume scheduler_monitor monitor -vv --time-limit 21600
|
||||||
|
numprocs={{ replicas|default(1) }}
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
5
templates/config/supervisord.conf.twig
Normal file
5
templates/config/supervisord.conf.twig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/run/supervisord.pid
|
||||||
@@ -15,6 +15,12 @@
|
|||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js'></script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
<div class="mb-4 text-white">
|
||||||
|
<a href="{{ path('app.monitors') }}" class="btn btn-primary inline-flex items-center gap-2">
|
||||||
|
<twig:ux:icon name="fluent-mdl2:back" width="14" /> Back to Monitors
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<twig:Card title="Upcoming episodes of shows your monitoring">
|
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||||
<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" />
|
||||||
@@ -34,6 +40,7 @@
|
|||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const modal = document.getElementById('previewModal');
|
const modal = document.getElementById('previewModal');
|
||||||
|
modal.setAttribute('mdWidth', '25rem');
|
||||||
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'];
|
||||||
|
|
||||||
@@ -47,7 +54,12 @@
|
|||||||
eventClick: function (data) {
|
eventClick: function (data) {
|
||||||
modal.display({
|
modal.display({
|
||||||
heading: data.event.title,
|
heading: data.event.title,
|
||||||
content: `<p>${data.event.startStr}</p>`
|
content: `
|
||||||
|
<div class="flex flex-col gap-4 justify-center items-center">
|
||||||
|
<img src="${data.event.extendedProps.attachment}" class="w-[90%] rounded-md" />
|
||||||
|
<p>${data.event.startStr}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
151
templates/monitor/view.html.twig
Normal file
151
templates/monitor/view.html.twig
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}{{ monitor.title }} — Monitors — Torsearch{% endblock %}
|
||||||
|
{% block h2 %}Monitors{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="px-4 py-2">
|
||||||
|
<div class="mb-4 text-white">
|
||||||
|
<a href="{{ path('app.monitors') }}" class="btn btn-primary inline-flex items-center gap-2">
|
||||||
|
<twig:ux:icon name="fluent-mdl2:back" width="14" /> Back to Monitors
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<twig:Card title="Viewing your monitors for {{ monitor.title }}">
|
||||||
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
|
{% if results.media.poster != null %}
|
||||||
|
<img class="w-full md:w-[12.5rem] rounded-lg" src="{{ results.media.poster }}" />
|
||||||
|
{% else %}
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col">
|
||||||
|
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||||
|
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
|
{{ results.media.title }} ({{ results.media.year }})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-50 mb-4">
|
||||||
|
{{ results.media.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-gray-50 mb-2">
|
||||||
|
<div id="people" class="mb-1">
|
||||||
|
{% if results.media.stars != null %}
|
||||||
|
<strong>Starring</strong>: {{ results.media.stars|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.directors != null %}
|
||||||
|
<strong>Directors</strong>: {{ results.media.directors|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.producers != null %}
|
||||||
|
<strong>Producers</strong>: {{ results.media.producers|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results.media.creators != null %}
|
||||||
|
<strong>Creators</strong>: {{ results.media.creators|join(', ') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dates" class="mb-1">
|
||||||
|
{% if results.media.premiereDate %}
|
||||||
|
<strong>Premiered</strong>: {{ results.media.premiereDate|date('n/j/Y', 'UTC') }} <br />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results.media.genres != null %}
|
||||||
|
<div id="genres" class="text-gray-50 my-4">
|
||||||
|
{% for genre in results.media.genres %}
|
||||||
|
<small class="px-2 py-1 border border-orange-500 rounded-full">{{ genre }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col grow text-white">
|
||||||
|
<strong class="mb-1">In Your Library</strong>
|
||||||
|
<div class="flex flex-col md:flex-row border-t-orange-500 text-xs gap-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Seasons</span>
|
||||||
|
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.seasonCount }}</span> full
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.partialSeasonCount }}</span> partial
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.missingSeasonCount }}</span> missing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Episodes</span>
|
||||||
|
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.episodeCount }}</span> existing
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-cyan-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.missingEpisodeCount }}</span> missing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm mb-1">Monitors</span>
|
||||||
|
<div class="flex flex-row border p-2 border-orange-500 rounded-lg text-xs items-center">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.monitorCount }}</span> total
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.activeMonitorCount }}</span> active
|
||||||
|
</span>
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-red-500 rounded-lg text-white">
|
||||||
|
<span>{{ library.completeMonitorCount }}</span> complete
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "movies" == results.media.mediaType %}
|
||||||
|
<div class="flex flex-row justify-start items-end grow text-xs">
|
||||||
|
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
|
<span class="results-count-number" id="movie_results_count">-</span> results
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<twig:Turbo:Frame id="meb_{{ results.media.imdbId }}" src="{{ path('api.library.search', {
|
||||||
|
title: results.media.title,
|
||||||
|
block: 'media_exists_badge',
|
||||||
|
target: "meb_" ~ results.media.imdbId
|
||||||
|
}) }}">
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Movie has not been downloaded yet.">
|
||||||
|
missing
|
||||||
|
</span>
|
||||||
|
</twig:Turbo:Frame>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-sky-700 rounded-lg text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||||
|
{{ results.media.premiereDate|date('n/j/Y', 'UTC') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold bg-orange-500 rounded-lg text-white" title="This movie has a runtime of {{ results.media.runtime }} minutes.">
|
||||||
|
{{ results.media.runtime }} minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<twig:MonitorList :parentMonitorId="monitor.id" :isWidget="false" :perPage="10"></twig:MonitorList>
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -86,9 +86,18 @@
|
|||||||
{{ result.languageFlags|raw }}
|
{{ result.languageFlags|raw }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
||||||
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50">
|
<a href="{{ url('app.torrentio.stream', {url: result.url|base64_encode}) }}" title="Stream in your browser." class="p-1.5 bg-blue-600 rounded-md text-gray-50">
|
||||||
Download
|
<twig:ux:icon name="bi:camera-video" width="20"/>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
|
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50" title="Download to your mounted directory.">
|
||||||
|
<twig:ux:icon name="bi:cloud-download" width="20" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="{{ result.url }}" title="Download to your local device." class="p-1.5 bg-orange-500 rounded-md text-gray-50">
|
||||||
|
<twig:ux:icon name="bi:download" width="20" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<label for="select">
|
<label for="select">
|
||||||
<input id="select" type="checkbox" name="select" />
|
<input id="select" type="checkbox" name="select" />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
7
templates/torrentio/stream.html.twig
Normal file
7
templates/torrentio/stream.html.twig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Streaming — Torrentio{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<iframe width="100%" height="100%" src="{{ stream_url }}"></iframe>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user