Compare commits
128 Commits
dev-upcomi
...
v0.38.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2effa0fb07 | ||
|
|
c2474942a1 | ||
|
|
c175dddede | ||
|
|
0e1d8e15e3 | ||
|
|
d38f8ba4be | ||
|
|
e20def1325 | ||
|
|
ed69ed61b8 | ||
|
|
9c8e625316 | ||
|
|
ef6ed20a0b | ||
|
|
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 | ||
|
|
0f291aa147 | ||
|
|
c4160081a1 | ||
|
|
5d414590cb | ||
|
|
d28b743684 | ||
|
|
c4e8e9b35e | ||
|
|
6fbd56c952 | ||
|
|
f5732fbcea | ||
|
|
0f095ab7f8 | ||
|
|
2e376337fa | ||
|
|
fc203f1bd3 | ||
|
|
2eda8e0808 | ||
| d3431b76e2 | |||
| a978469564 | |||
| 3097189c49 | |||
| 6f11de70e0 | |||
| 4e06fe6636 | |||
| fd46abf58f | |||
| 2237a45d6f | |||
| 846de2c257 | |||
| d01b725435 | |||
| 7562597629 | |||
| deb0333635 | |||
| c8e190f9e8 | |||
| 538fde40fe | |||
| da7a267e2a | |||
| daf9b2c18b | |||
| d9e5e62f5d | |||
| d4fc7693e3 | |||
| 1263ad20a6 | |||
| e8764bb13b | |||
| a267bab86e | |||
| 9653189bff | |||
| 36836c4d36 | |||
| 61e4b25212 | |||
| 209266597e | |||
| ca89eff236 | |||
| 53da7a746b | |||
| 981699bc13 | |||
| 52f460ff62 | |||
| a42e0d4d1a | |||
| 3e4a2d9bb1 | |||
| af8a30826c | |||
| 09e1c75826 | |||
| f1b8b34359 | |||
| 6f9db68664 | |||
| aeb706b5af | |||
| 7918c260e5 | |||
| 38130ea0ec | |||
| da403958dc | |||
| c2bafabb20 | |||
| e6983aedf9 | |||
| e9edd6a35a | |||
| ee076518b3 | |||
| a2f16398be | |||
| 0f03199eb4 | |||
| d63d477ed1 | |||
| 458229c7ed | |||
| 6748188256 | |||
| b42924048f | |||
| c0f1473037 | |||
| fc797a3a0f | |||
| b8b71fa5b3 | |||
| 662e2600f6 | |||
| aa042e8275 | |||
| 57498b1abf | |||
| fed1e1e122 | |||
| 9eef567974 | |||
| 070723581a | |||
| f3a5c2012e | |||
| 5581a82554 | |||
| 3703272f59 | |||
| b587302b30 | |||
| e5bab8e6fd | |||
| 502b85dda4 | |||
| 9c430290e9 | |||
| 583591bf4f |
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
.git
|
||||
.idea
|
||||
.phpunit.cache
|
||||
.php-cs-fixer.cache
|
||||
.env.test
|
||||
.gitignore
|
||||
bolt.db
|
||||
bash
|
||||
build.xml
|
||||
deploy.compose.yml
|
||||
phpstan.dist.neon
|
||||
phpunit.dist.xml
|
||||
nomad.deploy.hcl
|
||||
deployment.properties
|
||||
var/download/*
|
||||
12
.env
@@ -57,4 +57,14 @@ OIDC_BYPASS_FORM_LOGIN=false
|
||||
###< symfony/ntfy-notifier ###
|
||||
|
||||
NOTIFICATION_TRANSPORT=
|
||||
NTFY_DNS=
|
||||
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
|
||||
|
||||
24
.gitea/workflows/sonarqube_scans.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: SonarQube Scan
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube Trigger
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: SonarQube Scan
|
||||
uses: https://code.caldwell.digital/tools/sonarqube-action@v0.0.3
|
||||
with:
|
||||
host: "https://qube.caldwell.digital"
|
||||
login: ${{ secrets.SONARQUBE_TOKEN }}
|
||||
projectName: "torsearch"
|
||||
projectBaseDir: "./src"
|
||||
15
Dockerfile
@@ -1,19 +1,10 @@
|
||||
FROM dunglas/frankenphp:php8.4
|
||||
FROM code.caldwell.digital/home/torsearch-base:php8.4
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
ENV APP_VERSION="0.0.1"
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
RUN apt update && apt install -y wget
|
||||
ENV APP_VERSION="0.0.0-dev"
|
||||
|
||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
|
||||
|
||||
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||
@@ -67,6 +67,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
||||
<th class="px-4 py-2">
|
||||
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
|
||||
</th>
|
||||
<th class="px-4 py-2">
|
||||
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Air Date</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -107,6 +110,9 @@ export default class MonitorListRow extends HTMLTableRowElement {
|
||||
<td class="px-4 py-2">
|
||||
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -28,6 +28,9 @@ export default class PreviewContentDialog extends HTMLDialogElement {
|
||||
}
|
||||
|
||||
display({ heading, content }) {
|
||||
if (this.hasAttribute('mdWidth')) {
|
||||
this.style.width = this.getAttribute('mdWidth');
|
||||
}
|
||||
this.setHeading(heading);
|
||||
this.setContent(content);
|
||||
this.showModal();
|
||||
|
||||
54
assets/controllers/discover_media_results_controller.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
|
||||
*/
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
static targets = ['poster', 'moreBtn', 'moreLink']
|
||||
|
||||
moreResultsClicks = 0;
|
||||
|
||||
initialize() {
|
||||
// Called once when the controller is first instantiated (per element)
|
||||
|
||||
// Here you can initialize variables, create scoped callables for event
|
||||
// listeners, instantiate external libraries, etc.
|
||||
// this._fooBar = this.fooBar.bind(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Called every time the controller is connected to the DOM
|
||||
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
|
||||
|
||||
// Here you can add event listeners on the element or target elements,
|
||||
// add or remove classes, attributes, dispatch custom events, etc.
|
||||
// this.fooTarget.addEventListener('click', this._fooBar)
|
||||
}
|
||||
|
||||
// Add custom controller actions here
|
||||
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||
|
||||
disconnect() {
|
||||
// Called anytime its element is disconnected from the DOM
|
||||
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||
|
||||
// Here you should remove all event listeners added in "connect()"
|
||||
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||
}
|
||||
|
||||
moreResults() {
|
||||
const elems = this.posterTargets.filter(poster => poster.classList.contains('hidden'));
|
||||
if (this.moreResultsClicks <= 2) {
|
||||
elems.slice(0, 6).forEach(poster => poster.classList.remove('hidden'));
|
||||
this.moreResultsClicks++;
|
||||
|
||||
if (this.moreResultsClicks === 2) {
|
||||
this.moreBtnTarget.classList.add('hidden');
|
||||
this.moreLinkTarget.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,34 +13,7 @@ export default class extends Controller {
|
||||
tmdbId: String,
|
||||
imdbId: String,
|
||||
title: String,
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Called once when the controller is first instantiated (per element)
|
||||
|
||||
// Here you can initialize variables, create scoped callables for event
|
||||
// listeners, instantiate external libraries, etc.
|
||||
// this._fooBar = this.fooBar.bind(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Called every time the controller is connected to the DOM
|
||||
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
|
||||
|
||||
// Here you can add event listeners on the element or target elements,
|
||||
// add or remove classes, attributes, dispatch custom events, etc.
|
||||
// this.fooTarget.addEventListener('click', this._fooBar)
|
||||
}
|
||||
|
||||
// Add custom controller actions here
|
||||
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
|
||||
|
||||
disconnect() {
|
||||
// Called anytime its element is disconnected from the DOM
|
||||
// (on page change, when it's removed from or moved in the DOM, etc.)
|
||||
|
||||
// Here you should remove all event listeners added in "connect()"
|
||||
// this.fooTarget.removeEventListener('click', this._fooBar)
|
||||
season: Number,
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -53,34 +26,13 @@ export default class extends Controller {
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvshows',
|
||||
season: this.seasonValue
|
||||
});
|
||||
if (this.hasDialogOutlet) {
|
||||
this.dialogOutlet.close();
|
||||
}
|
||||
}
|
||||
|
||||
async monitorSeason() {
|
||||
await this.makeMonitor({
|
||||
tmdbId: this.tmdbIdValue,
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvseason',
|
||||
season: this.resultFilterOutlet.activeFilter['season'],
|
||||
});
|
||||
}
|
||||
|
||||
async monitorEpisode() {
|
||||
// ToDo: figure out how to set episode
|
||||
await this.makeMonitor({
|
||||
tmdbId: this.tmdbIdValue,
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvepisode',
|
||||
season: this.resultFilterOutlet.activeFilter['season'],
|
||||
episode: '',
|
||||
});
|
||||
}
|
||||
|
||||
async makeMonitor(body) {
|
||||
const response = await fetch('/api/monitor', {
|
||||
method: 'POST',
|
||||
@@ -90,7 +42,6 @@ export default class extends Controller {
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,12 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
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}}))
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ export default class extends Controller {
|
||||
};
|
||||
|
||||
static targets = ['list']
|
||||
static outlets = ['loading-icon']
|
||||
|
||||
options = []
|
||||
optionsLoaded = false
|
||||
@@ -28,7 +27,6 @@ export default class extends Controller {
|
||||
this.options = this.element.querySelectorAll('tbody tr');
|
||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||
this.resultCountEl.innerText = this.options.length;
|
||||
this.loadingIconOutlet.toggleIcon();
|
||||
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default class extends Controller {
|
||||
defaultOptions = '<option value="-">-</option>';
|
||||
|
||||
static outlets = ['tv-episode-list']
|
||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected']
|
||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected', 'currentSeason']
|
||||
static values = {
|
||||
'imdbId': String,
|
||||
'media-type': String,
|
||||
@@ -32,7 +32,6 @@ export default class extends Controller {
|
||||
|
||||
async connect() {
|
||||
await this.setInitialFilter();
|
||||
this.setTimerToStopLoadingIcon();
|
||||
this.element.filterResults = this.filter.bind(this);
|
||||
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
|
||||
}
|
||||
@@ -48,10 +47,6 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
setTimerToStopLoadingIcon() {
|
||||
setTimeout(() => this.loadingIconTarget.hideIcon(), 10000);
|
||||
}
|
||||
|
||||
// Event is fired from movies/tvshows controllers to populate this data
|
||||
async loadOptions({detail: { options }}) {
|
||||
await options.forEach((option) => {
|
||||
@@ -99,7 +94,9 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
setSeason(event) {
|
||||
console.log('hurrrr');
|
||||
this.tvEpisodeListOutlet.setSeason(event.target.value);
|
||||
this.currentSeasonTarget.innerText = event.target.value;
|
||||
}
|
||||
|
||||
downloadSeason() {
|
||||
|
||||
@@ -38,21 +38,21 @@ export default class extends Controller {
|
||||
return `
|
||||
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
|
||||
</span>
|
||||
`;
|
||||
</span>`;
|
||||
}
|
||||
event.detail.options.render.option = (data, escape) => {
|
||||
if (data.data.description.length > 60) {
|
||||
data.data.description = data.data.description.substring(0, 107) + "...";
|
||||
console.log(data);
|
||||
if (data.data.overview.length > 60) {
|
||||
data.data.overview = data.data.overview.substring(0, 107) + "...";
|
||||
}
|
||||
|
||||
return `<div class="flex flex-row">
|
||||
<img src="${data.data.poster}" class="w-16 rounded-md">
|
||||
<div class="p-2 flex flex-col">
|
||||
<h2>${data.data.title}</h2>
|
||||
<p class="max-w-[60ch] text-wrap">${data.data.description}</p>
|
||||
<p class="max-w-[60ch] text-wrap">${data.data.overview}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class extends Controller {
|
||||
};
|
||||
|
||||
static targets = ['list', 'count', 'episodeSelector',]
|
||||
static outlets = ['loading-icon']
|
||||
|
||||
options = []
|
||||
|
||||
@@ -35,6 +34,5 @@ export default class extends Controller {
|
||||
this.countTarget.innerText = 0;
|
||||
this.episodeSelectorTarget.disabled = true;
|
||||
}
|
||||
this.loadingIconOutlet.increaseCount();
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
assets/icons/lets-icons/calendar-add-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 529 B |
1
assets/icons/solar/calendar-linear.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="currentColor" stroke-width="1.5" d="M2 12c0-3.771 0-5.657 1.172-6.828S6.229 4 10 4h4c3.771 0 5.657 0 6.828 1.172S22 8.229 22 12v2c0 3.771 0 5.657-1.172 6.828S17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.172S2 17.771 2 14z"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M7 4V2.5M17 4V2.5M2.5 9h19"/><path fill="currentColor" d="M18 17a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 653 B |
@@ -29,6 +29,7 @@
|
||||
|
||||
:root {
|
||||
--fc-border-color: #a65b27;
|
||||
--fc-page-bg-color: #a65b27;
|
||||
}
|
||||
|
||||
/* Prevent scrolling while dialog is open */
|
||||
@@ -198,6 +199,10 @@ dialog[data-dialog-target="dialog"][closing] {
|
||||
@apply flex flex-col gap-1 justify-between;
|
||||
}
|
||||
|
||||
/** FullCalendar **/
|
||||
#upcoming_episodes_calendar .fc-event-main .fc-event-title-container {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.fc-col-header-cell {
|
||||
@apply bg-orange-500/60 text-white;
|
||||
}
|
||||
|
||||
22
bash/build_base.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
# torsearch-app is built from this base
|
||||
export APP_FRANKENPHP_TAG=php8.4
|
||||
|
||||
#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:latest
|
||||
|
||||
# torsearch-worker & torsearch-scheduler are built from this base
|
||||
export WORKER_FRANKENPHP_TAG=php8.4-alpine
|
||||
|
||||
#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: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
@@ -33,7 +33,9 @@ services:
|
||||
|
||||
|
||||
worker:
|
||||
build: .
|
||||
build:
|
||||
dockerfile: docker/Dockerfile.worker
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
@@ -41,18 +43,19 @@ services:
|
||||
tty: true
|
||||
environment:
|
||||
TZ: America/Chicago
|
||||
command: php /app/bin/console messenger:consume async -vv --time-limit=3600
|
||||
|
||||
|
||||
scheduler:
|
||||
build: .
|
||||
build:
|
||||
dockerfile: docker/Dockerfile.scheduler
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD:/app
|
||||
- $PWD/var/download:/var/download
|
||||
tty: true
|
||||
environment:
|
||||
TZ: America/Chicago
|
||||
command: php /app/bin/console messenger:consume scheduler_monitor -vv
|
||||
tty: true
|
||||
WORKER_MONITOR: 2
|
||||
|
||||
|
||||
redis:
|
||||
@@ -88,6 +91,8 @@ services:
|
||||
image: adminer
|
||||
ports:
|
||||
- "8081:8080"
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: database
|
||||
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -25,8 +25,12 @@
|
||||
"p3k/emoji-detector": "^1.2",
|
||||
"php-http/cache-plugin": "^2.0",
|
||||
"php-tmdb/api": "^4.1",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"predis/predis": "^2.4",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"sentry/sentry-symfony": "^5.6",
|
||||
"spatie/icalendar-generator": "^3.0",
|
||||
"spomky-labs/pwa-bundle": "^1.2",
|
||||
"stof/doctrine-extensions-bundle": "^1.14",
|
||||
"symfony/asset": "7.3.*",
|
||||
@@ -43,11 +47,16 @@
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/mercure-bundle": "^0.3.9",
|
||||
"symfony/messenger": "7.3.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/notifier": "7.3.*",
|
||||
"symfony/ntfy-notifier": "7.3.*",
|
||||
"symfony/object-mapper": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/scheduler": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/stimulus-bundle": "^2.24",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-autocomplete": "^2.27",
|
||||
|
||||
733
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6f57ba35ae317ec6370836bda0012db8",
|
||||
"content-hash": "952def2c32d975032ac0061e5aa37319",
|
||||
"packages": [
|
||||
{
|
||||
"name": "1tomany/rich-bundle",
|
||||
@@ -2552,6 +2552,66 @@
|
||||
],
|
||||
"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",
|
||||
"version": "5.5.0",
|
||||
@@ -2681,6 +2741,109 @@
|
||||
},
|
||||
"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",
|
||||
"version": "3.10.0",
|
||||
@@ -4757,6 +4920,255 @@
|
||||
],
|
||||
"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",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/icalendar-generator.git",
|
||||
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"larapack/dd": "^1.1",
|
||||
"nesbot/carbon": "^3.5",
|
||||
"pestphp/pest": "^2.34",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"spatie/pest-plugin-snapshots": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\IcalendarGenerator\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ruben Van Assche",
|
||||
"email": "ruben@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Build calendars in the iCalendar format",
|
||||
"homepage": "https://github.com/spatie/icalendar-generator",
|
||||
"keywords": [
|
||||
"calendar",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/icalendar-generator/issues",
|
||||
"source": "https://github.com/spatie/icalendar-generator/tree/3.0.0"
|
||||
},
|
||||
"time": "2025-04-17T14:50:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/pki-framework",
|
||||
"version": "1.3.0",
|
||||
@@ -7578,6 +7990,169 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v7.3.0",
|
||||
@@ -7724,6 +8299,79 @@
|
||||
],
|
||||
"time": "2025-02-13T10:27:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/object-mapper",
|
||||
"version": "v7.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/object-mapper.git",
|
||||
"reference": "f7f9833d9fcc8361239c1dae5495aa9e43ece0b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/object-mapper/zipball/f7f9833d9fcc8361239c1dae5495aa9e43ece0b5",
|
||||
"reference": "f7f9833d9fcc8361239c1dae5495aa9e43ece0b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^2.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/property-access": "<7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^7.2",
|
||||
"symfony/var-exporter": "^7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\ObjectMapper\\": ""
|
||||
},
|
||||
"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 a way to map an object to another object",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/object-mapper/tree/v7.3.3"
|
||||
},
|
||||
"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-08-13T14:03:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.3.0",
|
||||
@@ -8645,6 +9293,89 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v7.3.0",
|
||||
|
||||
@@ -23,4 +23,6 @@ return [
|
||||
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
|
||||
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
|
||||
Sentry\SentryBundle\SentryBundle::class => ['prod' => true, 'dev' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -41,7 +41,13 @@ doctrine:
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Monitor/Framework/Entity'
|
||||
prefix: 'App\Monitor\Framework\Entity'
|
||||
alias: Download
|
||||
alias: Monitor
|
||||
EventLog:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/EventLog/Framework/Entity'
|
||||
prefix: 'App\EventLog\Framework\Entity'
|
||||
alias: EventLog
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
|
||||
@@ -15,6 +15,26 @@ framework:
|
||||
max_retries: 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:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
@@ -36,12 +56,12 @@ framework:
|
||||
routing:
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
'App\Download\Action\Command\DownloadMediaCommand': async
|
||||
'App\Download\Action\Command\DownloadMediaCommand': download
|
||||
'App\Download\Action\Command\DownloadSeasonCommand': async
|
||||
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': async
|
||||
'App\Monitor\Action\Command\MonitorTvSeasonCommand': async
|
||||
'App\Monitor\Action\Command\MonitorTvShowCommand': async
|
||||
'App\Monitor\Action\Command\MonitorMovieCommand': async
|
||||
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': monitor
|
||||
'App\Monitor\Action\Command\MonitorTvSeasonCommand': monitor
|
||||
'App\Monitor\Action\Command\MonitorTvShowCommand': monitor
|
||||
'App\Monitor\Action\Command\MonitorMovieCommand': monitor
|
||||
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': media_cache
|
||||
|
||||
# when@test:
|
||||
|
||||
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
|
||||
@@ -45,6 +45,7 @@ security:
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||
|
||||
40
config/packages/sentry.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
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:
|
||||
release: 'torsearch@%app.version%'
|
||||
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:
|
||||
globals:
|
||||
version: '%app.version%'
|
||||
sentry_javascript_url: '%sentry.javascript_url%'
|
||||
file_name_pattern: '*.twig'
|
||||
date:
|
||||
format: 'm/d/Y'
|
||||
|
||||
@@ -6,6 +6,22 @@ controllersBase:
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersDiscover:
|
||||
resource:
|
||||
path: ../src/Discover/Framework/Controller/
|
||||
namespace: App\Discover\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersEventLog:
|
||||
resource:
|
||||
path: ../src/EventLog/Framework/Controller/
|
||||
namespace: App\EventLog\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersLibrary:
|
||||
resource:
|
||||
path: ../src/Library/Framework/Controller/
|
||||
|
||||
@@ -32,7 +32,7 @@ parameters:
|
||||
app.cache.redis.host.default: 'redis://redis'
|
||||
|
||||
# Various configs
|
||||
app.default.version: '0.dev'
|
||||
app.default.version: '0.0.0-dev'
|
||||
app.default.timezone: 'America/Chicago'
|
||||
|
||||
# Auth
|
||||
@@ -48,6 +48,10 @@ parameters:
|
||||
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
|
||||
notification.ntfy.dsn: '%env(NTFY_DSN)%'
|
||||
|
||||
# Sentry
|
||||
sentry.dsn: '%env(SENTRY_DSN)%'
|
||||
sentry.javascript_url: '%env(SENTRY_JS_URL)%'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
app:
|
||||
image: registry.caldwell.digital/home/torsearch-app:${TAG}
|
||||
ports:
|
||||
- '8001:80'
|
||||
- "${SWARM_PORT}:80"
|
||||
environment:
|
||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
@@ -13,12 +13,6 @@ services:
|
||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
depends_on:
|
||||
- database
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12202"
|
||||
|
||||
|
||||
worker:
|
||||
@@ -26,16 +20,10 @@ services:
|
||||
volumes:
|
||||
- /mnt/media/downloads/movies:/var/download/movies
|
||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||
restart: always
|
||||
command: -vv --time-limit=3600 --limit=10
|
||||
deploy:
|
||||
replicas: 2
|
||||
depends_on:
|
||||
- app
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12203"
|
||||
|
||||
|
||||
scheduler:
|
||||
@@ -43,28 +31,10 @@ services:
|
||||
volumes:
|
||||
- /mnt/media/downloads/movies:/var/download/movies
|
||||
- /mnt/media/downloads/tvshows:/var/download/tvshows
|
||||
restart: always
|
||||
command: -vv
|
||||
depends_on:
|
||||
- app
|
||||
logging:
|
||||
driver: "gelf"
|
||||
options:
|
||||
gelf-address: "tcp://192.168.1.197:12204"
|
||||
|
||||
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --maxmemory 512MB
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
volumes:
|
||||
mysql:
|
||||
mercure_config:
|
||||
mercure_data:
|
||||
redis_data:
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ templates
|
||||
var
|
||||
vendor
|
||||
build.xml
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
composer.json
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
FROM dunglas/frankenphp
|
||||
FROM code.caldwell.digital/home/torsearch-base:php8.4
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
|
||||
ARG APP_VERSION="0.dev"
|
||||
ARG APP_VERSION="0.0.0-dev"
|
||||
ENV APP_VERSION="${APP_VERSION}"
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
COPY . /app
|
||||
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
|
||||
|
||||
14
docker/Dockerfile.base.app
Normal file
@@ -0,0 +1,14 @@
|
||||
ARG FRANKENPHP_TAG
|
||||
|
||||
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
40
docker/Dockerfile.base.worker
Normal file
@@ -0,0 +1,40 @@
|
||||
#####
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
RUN ln -s /usr/bin/php84 /usr/bin/php
|
||||
|
||||
RUN mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# We start supervisord and supervisord starts
|
||||
# respective service in the configuration file.
|
||||
ENTRYPOINT ["/app/bin/console", "init:worker"]
|
||||
|
||||
# Message transports can be enabled by passing them as command options.
|
||||
# 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
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
FROM dunglas/frankenphp: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.
|
||||
###
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
# Default to latest, but should pass in a version
|
||||
ARG APP_VERSION="latest"
|
||||
|
||||
ARG APP_VERSION="0.dev"
|
||||
ENV APP_VERSION="${APP_VERSION}"
|
||||
# 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
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
# Start with our base worker image
|
||||
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
|
||||
|
||||
COPY . /app
|
||||
# Set the APP_VERSION in the image
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||
# Copy the actual application code from the previously built app
|
||||
COPY --chown=1000:1000 --from=app_image /app /app
|
||||
|
||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0
|
||||
# To retain backwards compatibility, default to async & download transports
|
||||
CMD [ "--monitor" ]
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
FROM dunglas/frankenphp: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.
|
||||
###
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||
# Default to latest, but should pass in a version
|
||||
ARG APP_VERSION="latest"
|
||||
|
||||
ARG APP_VERSION="0.dev"
|
||||
ENV APP_VERSION="${APP_VERSION}"
|
||||
# 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
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
# Start with our base worker image
|
||||
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
|
||||
|
||||
RUN apk add --no-cache wget
|
||||
# Set the APP_VERSION in the image
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
COPY . /app
|
||||
# Copy the actual application code from the previously built app
|
||||
COPY --chown=1000:1000 --from=app_image /app /app
|
||||
|
||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
|
||||
|
||||
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0
|
||||
# To retain backwards compatibility, default to async & download transports
|
||||
CMD [ "--async", "--download" ]
|
||||
|
||||
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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/run/supervisord.pid
|
||||
41
migrations/Version20250831013403.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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 Version20250831013403 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'
|
||||
DROP TABLE IF EXISTS sessions
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value 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'
|
||||
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);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20251101194723.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 Version20251101194723 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 user_preference CHANGE preference_value preference_value VARCHAR(255) 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 user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20251101211617.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 Version20251101211617 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'
|
||||
CREATE TABLE event_log (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) DEFAULT NULL, message LONGTEXT DEFAULT NULL, context JSON DEFAULT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE event_log
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
47
migrations/Version20251102004627.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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 Version20251102004627 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 user_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE event_log ADD CONSTRAINT FK_9EF0AD16A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_9EF0AD16A76ED395 ON event_log (user_id)
|
||||
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 FOREIGN KEY FK_9EF0AD16A76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_9EF0AD16A76ED395 ON event_log
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE event_log DROP user_id
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
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
@@ -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);
|
||||
}
|
||||
}
|
||||
17
psalm.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="7"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
findUnusedBaselineEntry="true"
|
||||
findUnusedCode="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@@ -2,16 +2,25 @@
|
||||
|
||||
namespace App\Base;
|
||||
|
||||
use App\Base\Dto\AppVersionDto;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
final class ConfigResolver
|
||||
{
|
||||
const SEMVER_REGEX = '/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/';
|
||||
|
||||
private array $messages = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
|
||||
#[Autowire(param: 'app.url')]
|
||||
private readonly ?string $appUrl = null,
|
||||
|
||||
#[Autowire(param: 'app.version')]
|
||||
private readonly ?string $appVersion = null,
|
||||
|
||||
#[Autowire(param: 'app.debrid.real_debrid.key')]
|
||||
private readonly ?string $realDebridApiKey = null,
|
||||
|
||||
@@ -44,6 +53,15 @@ final class ConfigResolver
|
||||
|
||||
#[Autowire(param: 'notification.ntfy.dsn')]
|
||||
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
|
||||
@@ -92,6 +110,13 @@ final class ConfigResolver
|
||||
return $this->authOidcBypassFormLogin;
|
||||
}
|
||||
|
||||
public function getAppVersion(): AppVersionDto
|
||||
{
|
||||
$matches = [];
|
||||
preg_match(self::SEMVER_REGEX, $this->appVersion, $matches);
|
||||
return $this->denormalizer->denormalize($matches, AppVersionDto::class);
|
||||
}
|
||||
|
||||
public function getAuthConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -104,4 +129,13 @@ final class ConfigResolver
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getSentryConfig()
|
||||
{
|
||||
return [
|
||||
'dsn' => $this->sentryDsn,
|
||||
'environment' => $this->sentryEnvironment,
|
||||
'javascript_url' => $this->sentryJavascriptUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
24
src/Base/Dto/AppVersionDto.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Base\Dto;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||
|
||||
class AppVersionDto
|
||||
{
|
||||
#[SerializedPath('[1]')]
|
||||
public string|int $major = 0;
|
||||
#[SerializedPath('[2]')]
|
||||
public string|int $minor = 0;
|
||||
#[SerializedPath('[3]')]
|
||||
public string|int $patch = 0;
|
||||
#[SerializedPath('[4]')]
|
||||
public ?string $pre = null;
|
||||
#[SerializedPath('[5]')]
|
||||
public ?string $build = null;
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return 'v' . $this->major . '.' . $this->minor . '.' . $this->patch . ($this->pre ? '-' . $this->pre : '') . ($this->build ? '+' . $this->build : '');
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ enum MediaType: string
|
||||
{
|
||||
case Movie = 'movies';
|
||||
case TvShow = 'tvshows';
|
||||
case TvEpisode = 'tvepisode';
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,13 @@ class SeedDatabaseCommand extends Command
|
||||
'enabled' => true,
|
||||
'type' => 'download'
|
||||
],
|
||||
[
|
||||
'id' => 'enable_ical_up_ep',
|
||||
'name' => 'Enable a publicly available iCal calendar?',
|
||||
'description' => 'Enable a publicly accessible iCal URL for your upcoming episodes.',
|
||||
'enabled' => false,
|
||||
'type' => 'calendar'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,29 @@ namespace App\Base\Framework\Controller;
|
||||
|
||||
use App\Monitor\Action\Command\MonitorTvShowCommand;
|
||||
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly MonitorTvShowHandler $monitorTvShowHandler,
|
||||
private readonly TmdbClient $tmdb,
|
||||
) {}
|
||||
|
||||
#[Route('/', name: 'app_index')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
return $this->render('index/index.html.twig', [
|
||||
'active_downloads' => $this->getUser()->getActiveDownloads(),
|
||||
'recent_downloads' => $this->getUser()->getDownloads(),
|
||||
'popular_movies' => $this->tmdb->popularMovies(1, 6),
|
||||
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
|
||||
'popular_movies' => $this->tmdb->popularMovies(),
|
||||
'popular_tvshows' => $this->tmdb->popularTvShows(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -51,8 +48,9 @@ final class IndexController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/test')]
|
||||
public function monitorTvShow(): Response
|
||||
public function monitorTvShow(MonitorTvShowHandler $handler): Response
|
||||
{
|
||||
throw new \Exception('Test');
|
||||
return $this->render('index/test.html.twig', []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,37 @@ class MediaFiles
|
||||
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)
|
||||
{
|
||||
$filepath = $this->moviesPath . DIRECTORY_SEPARATOR . $title;
|
||||
|
||||
@@ -6,6 +6,6 @@ class ImdbMatcher
|
||||
{
|
||||
public static function isMatch(string $imdbId): bool
|
||||
{
|
||||
return preg_match('/^tt\d{7}$/', $imdbId);
|
||||
return preg_match('/^tt\d{7,20}$/', $imdbId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ class Paginator
|
||||
public function paginate($query, int $page = 1, int $limit = 5): Paginator
|
||||
{
|
||||
$paginator = new OrmPaginator($query);
|
||||
|
||||
$paginator
|
||||
->getQuery()
|
||||
->setFirstResult($limit * ($page - 1))
|
||||
|
||||
141
src/Discover/Framework/Controller/WebController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Discover\Framework\Controller;
|
||||
|
||||
use App\Base\Enum\MediaType;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\Tmdb\TmdbMovieGenre;
|
||||
use App\Tmdb\TmdbTvShowGenre;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/discover')]
|
||||
class WebController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app.discover')]
|
||||
public function index(TmdbClient $tmdb)
|
||||
{
|
||||
$movies = $tmdb->popularMovies(18);
|
||||
$tvshows = $tmdb->popularTvShows(18);
|
||||
|
||||
return $this->render('discover/index.html.twig', [
|
||||
'movies' => $movies,
|
||||
'shows' => $tvshows,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{mediaType}/{genreId?}', name: 'app.discover.browse')]
|
||||
public function browse(string $mediaType, ?string $genreId, TmdbClient $tmdb)
|
||||
{
|
||||
if (null === $genreId) {
|
||||
if (MediaType::tryFrom($mediaType) === null) {
|
||||
return new Response(status: 404);
|
||||
}
|
||||
return $this->render('discover/browse.html.twig', [
|
||||
'genres' => self::getGenres($mediaType),
|
||||
'media_type' => $mediaType,
|
||||
]);
|
||||
}
|
||||
|
||||
if (MediaType::tryFrom($mediaType) === null) {
|
||||
return new Response(status: 404);
|
||||
}
|
||||
|
||||
if (TmdbMovieGenre::tryFrom($genreId) === null &&
|
||||
TmdbTvShowGenre::tryFrom($genreId) === null
|
||||
) {
|
||||
return new Response(status: 404);
|
||||
}
|
||||
|
||||
$results = match ($mediaType) {
|
||||
MediaType::Movie->value => $tmdb->discoverMovies(
|
||||
[TmdbMovieGenre::from($genreId)->value]
|
||||
),
|
||||
MediaType::TvShow->value => $tmdb->discoverTvShows(
|
||||
[TmdbTvShowGenre::from($genreId)->value]
|
||||
),
|
||||
};
|
||||
|
||||
return $this->render('discover/browse_genre.html.twig', [
|
||||
'media' => $results,
|
||||
'genre' => TmdbMovieGenre::from($genreId)->name,
|
||||
'genre_id' => $genreId,
|
||||
'media_type' => $mediaType,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{mediaType}/{genreId}', name: 'app.discover.browse_genre')]
|
||||
public function browseGenre(string $mediaType, string $genreId, TmdbClient $tmdb)
|
||||
{
|
||||
if (MediaType::tryFrom($mediaType) === null) {
|
||||
return new Response(status: 404);
|
||||
}
|
||||
|
||||
if (TmdbMovieGenre::tryFrom($genreId) === null &&
|
||||
TmdbTvShowGenre::tryFrom($genreId) === null
|
||||
) {
|
||||
return new Response(status: 404);
|
||||
}
|
||||
|
||||
$results = match ($mediaType) {
|
||||
MediaType::Movie->value => $tmdb->discoverMovies(
|
||||
[TmdbMovieGenre::from($genreId)->value]
|
||||
),
|
||||
MediaType::TvShow->value => $tmdb->discoverTvShows(
|
||||
[TmdbTvShowGenre::from($genreId)->value]
|
||||
),
|
||||
};
|
||||
|
||||
return $this->render('discover/browse.html.twig', [
|
||||
'media' => $results,
|
||||
'media_type' => $mediaType,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function getGenres(string $mediaType): array
|
||||
{
|
||||
return match ($mediaType) {
|
||||
MediaType::Movie->value => [
|
||||
'Action' => TmdbMovieGenre::Action->value,
|
||||
'Adventure' => TmdbMovieGenre::Adventure->value,
|
||||
'Animation' => TmdbMovieGenre::Animation->value,
|
||||
'Comedy' => TmdbMovieGenre::Comedy->value,
|
||||
'Crime' => TmdbMovieGenre::Crime->value,
|
||||
'Documentary' => TmdbMovieGenre::Documentary->value,
|
||||
'Drama' => TmdbMovieGenre::Drama->value,
|
||||
'Family' => TmdbMovieGenre::Family->value,
|
||||
'Fantasy' => TmdbMovieGenre::Fantasy->value,
|
||||
'History' => TmdbMovieGenre::History->value,
|
||||
'Horror' => TmdbMovieGenre::Horror->value,
|
||||
'Music' => TmdbMovieGenre::Music->value,
|
||||
'Mystery' => TmdbMovieGenre::Mystery->value,
|
||||
'Romance' => TmdbMovieGenre::Romance->value,
|
||||
'Science Fiction' => TmdbMovieGenre::ScienceFiction->value,
|
||||
'TV Movie' => TmdbMovieGenre::TvMovie->value,
|
||||
'Thriller' => TmdbMovieGenre::Thriller->value,
|
||||
'War' => TmdbMovieGenre::War->value,
|
||||
'Western' => TmdbMovieGenre::Western->value,
|
||||
],
|
||||
MediaType::TvShow->value => [
|
||||
'Action & Adventure' => TmdbTvShowGenre::ActionAndAdventure->value,
|
||||
'Animation' => TmdbTvShowGenre::Animation->value,
|
||||
'Comedy' => TmdbTvShowGenre::Comedy->value,
|
||||
'Crime' => TmdbTvShowGenre::Crime->value,
|
||||
'Documentary' => TmdbTvShowGenre::Documentary->value,
|
||||
'Drama' => TmdbTvShowGenre::Drama->value,
|
||||
'Family' => TmdbTvShowGenre::Family->value,
|
||||
'Kids' => TmdbTvShowGenre::Kids->value,
|
||||
'Mystery' => TmdbTvShowGenre::Mystery->value,
|
||||
'News' => TmdbTvShowGenre::News->value,
|
||||
'Reality' => TmdbTvShowGenre::Reality->value,
|
||||
'Sci-Fi & Fantasy' => TmdbTvShowGenre::SciFiAndFantasy->value,
|
||||
'Soap' => TmdbTvShowGenre::Soap->value,
|
||||
'Talk' => TmdbTvShowGenre::Talk->value,
|
||||
'War & Politics' => TmdbTvShowGenre::WarAndPolitics->value,
|
||||
'Western' => TmdbTvShowGenre::Western->value,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,22 @@ namespace App\Download\Action\Handler;
|
||||
|
||||
use App\Download\Action\Command\DeleteDownloadCommand;
|
||||
use App\Download\Action\Result\DeleteDownloadResult;
|
||||
use App\Download\DownloadEvents;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\Library\Action\Command\DeleteMediaFileCommand;
|
||||
use App\Library\Action\Handler\DeleteMediaFileHandler;
|
||||
use App\Monitor\MonitorEvents;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<DeleteDownloadCommand, DeleteDownloadResult> */
|
||||
readonly class DeleteDownloadHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MessageBusInterface $bus,
|
||||
private DownloadRepository $downloadRepository,
|
||||
private DeleteMediaFileHandler $deleteMediaFileHandler,
|
||||
) {}
|
||||
@@ -31,6 +36,13 @@ readonly class DeleteDownloadHandler implements HandlerInterface
|
||||
}
|
||||
$this->downloadRepository->delete($command->downloadId);
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$download->getUser(),
|
||||
DownloadEvents::DOWNLOAD_DELETED->type(),
|
||||
DownloadEvents::DOWNLOAD_DELETED->message(),
|
||||
(array) $download
|
||||
));
|
||||
|
||||
return new DeleteDownloadResult(
|
||||
status: 200,
|
||||
message: 'Success',
|
||||
|
||||
@@ -4,18 +4,22 @@ namespace App\Download\Action\Handler;
|
||||
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\Action\Result\DownloadMediaResult;
|
||||
use App\Download\DownloadEvents;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Download\Downloader\DownloaderInterface;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<DownloadMediaCommand, DownloadMediaResult> */
|
||||
readonly class DownloadMediaHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MessageBusInterface $bus,
|
||||
private DownloaderInterface $downloader,
|
||||
private DownloadRepository $downloadRepository,
|
||||
private UserRepository $userRepository,
|
||||
@@ -23,9 +27,17 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$user = $this->userRepository->find($command->userId);
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$user,
|
||||
DownloadEvents::DOWNLOAD_STARTED->type(),
|
||||
DownloadEvents::DOWNLOAD_STARTED->message(),
|
||||
(array) $command
|
||||
));
|
||||
|
||||
if (null === $command->downloadId) {
|
||||
$download = $this->downloadRepository->insert(
|
||||
$this->userRepository->find($command->userId),
|
||||
$user,
|
||||
$command->url,
|
||||
$command->title,
|
||||
$command->filename,
|
||||
@@ -57,6 +69,12 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
||||
throw new UnrecoverableMessageHandlingException($exception->getMessage(), 500);
|
||||
}
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$user,
|
||||
DownloadEvents::DOWNLOAD_FINISHED->type(),
|
||||
DownloadEvents::DOWNLOAD_FINISHED->message(),
|
||||
(array) $command
|
||||
));
|
||||
return new DownloadMediaResult(200, "Success.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
namespace App\Download\Action\Handler;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Base\Enum\MediaType;
|
||||
use App\Base\Service\MediaFiles;
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\Action\Command\DownloadSeasonCommand;
|
||||
use App\Download\Action\Result\DownloadMediaResult;
|
||||
use App\Download\Action\Result\DownloadSeasonResult;
|
||||
use App\Download\DownloadOptionEvaluator;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use App\User\Dto\UserPreferencesFactory;
|
||||
@@ -27,7 +28,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private MediaFiles $mediaFiles,
|
||||
private LoggerInterface $logger,
|
||||
private Tmdb $tmdb,
|
||||
private TmdbClient $tmdb,
|
||||
private MessageBusInterface $bus,
|
||||
private DownloadOptionEvaluator $downloadOptionEvaluator,
|
||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||
@@ -36,7 +37,8 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$series = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
$series = $this->tmdb->tvshowDetails($command->imdbId);
|
||||
|
||||
$this->logger->info('> [DownloadTvSeasonHandler] Executing DownloadTvSeasonHandler for "' . $series->title . '" season ' . $command->season);
|
||||
|
||||
$episodesInSeason = Map::from($series->episodes[$command->season]);
|
||||
@@ -44,14 +46,14 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
||||
|
||||
$downloadCommands = [];
|
||||
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(
|
||||
new GetTvShowOptionsCommand(
|
||||
$series->tmdbId,
|
||||
$command->imdbId,
|
||||
$command->season,
|
||||
$episode['episode_number']
|
||||
$episode->episodeNumber
|
||||
)
|
||||
);
|
||||
|
||||
@@ -65,7 +67,7 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
||||
|
||||
if (null !== $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(
|
||||
$result->url,
|
||||
$series->title,
|
||||
@@ -97,10 +99,10 @@ readonly class DownloadSeasonHandler implements HandlerInterface
|
||||
->filter(fn ($episode) =>
|
||||
property_exists($episode, 'episode')
|
||||
&& property_exists($episode, 'season')
|
||||
&& null !== $episode->episode
|
||||
&& null !== $episode->episodeNumber
|
||||
&& 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());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Download/DownloadEvents.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download;
|
||||
|
||||
enum DownloadEvents
|
||||
{
|
||||
case DOWNLOAD_ADDED;
|
||||
case DOWNLOAD_STARTED;
|
||||
case DOWNLOAD_FINISHED;
|
||||
case DOWNLOAD_DELETED;
|
||||
case DOWNLOAD_ERROR;
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DOWNLOAD_ADDED => 'download_added',
|
||||
self::DOWNLOAD_STARTED => 'download_started',
|
||||
self::DOWNLOAD_FINISHED => 'download_finished',
|
||||
self::DOWNLOAD_DELETED => 'download_deleted',
|
||||
self::DOWNLOAD_ERROR => 'download_error',
|
||||
};
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DOWNLOAD_ADDED => 'A new download has been added.',
|
||||
self::DOWNLOAD_STARTED => 'A download has started.',
|
||||
self::DOWNLOAD_FINISHED => 'A download has finished.',
|
||||
self::DOWNLOAD_DELETED => 'A download has been deleted.',
|
||||
self::DOWNLOAD_ERROR => 'A download has encountered an error.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,12 @@ namespace App\Download\Downloader;
|
||||
|
||||
use App\Base\Service\Broadcaster;
|
||||
use App\Base\Service\MediaFiles;
|
||||
use App\Download\DownloadEvents;
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
@@ -17,6 +20,7 @@ class ProcessDownloader implements DownloaderInterface
|
||||
* @var RedisAdapter $cache
|
||||
*/
|
||||
public function __construct(
|
||||
private MessageBusInterface $bus,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MediaFiles $mediaFiles,
|
||||
private CacheInterface $cache,
|
||||
@@ -88,6 +92,12 @@ class ProcessDownloader implements DownloaderInterface
|
||||
}
|
||||
} catch (ProcessFailedException $exception) {
|
||||
$downloadEntity->setStatus('Failed');
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$downloadEntity->getUser()->getId(),
|
||||
DownloadEvents::DOWNLOAD_ERROR->type(),
|
||||
DownloadEvents::DOWNLOAD_ERROR->message() . ': ' . $exception->getMessage(),
|
||||
(array) $downloadEntity
|
||||
));
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -11,7 +11,9 @@ use App\Download\Action\Input\DownloadMediaInput;
|
||||
use App\Download\Action\Input\DownloadSeasonInput;
|
||||
use App\Download\Action\Input\PauseDownloadInput;
|
||||
use App\Download\Action\Input\ResumeDownloadInput;
|
||||
use App\Download\DownloadEvents;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
@@ -41,6 +43,13 @@ class ApiController extends AbstractController
|
||||
$input->downloadId = $download->getId();
|
||||
$input->userId = $this->getUser()->getId();
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$this->getUser(),
|
||||
DownloadEvents::DOWNLOAD_ADDED->type(),
|
||||
DownloadEvents::DOWNLOAD_ADDED->message(),
|
||||
(array) $download
|
||||
));
|
||||
|
||||
try {
|
||||
$this->bus->dispatch($input->toCommand());
|
||||
} catch (\Throwable $exception) {
|
||||
|
||||
17
src/EventLog/Action/Command/AddEventLogCommand.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/** @implements CommandInterface<AddEventLogCommand> */
|
||||
class AddEventLogCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public UserInterface $user,
|
||||
public string $type,
|
||||
public string $message,
|
||||
public array $context,
|
||||
) {}
|
||||
}
|
||||
29
src/EventLog/Action/Handler/AddEventLogHandler.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Action\Handler;
|
||||
|
||||
use App\EventLog\Framework\Repository\EventLogRepository;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\EventLog\Action\Result\AddEventLogResult;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/*** @implements HandlerInterface<AddEventLogCommand> */
|
||||
class AddEventLogHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EventLogRepository $repository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$eventLog = $this->repository->insert(
|
||||
user: $command->user,
|
||||
type: $command->type,
|
||||
message: $command->message,
|
||||
context: $command->context
|
||||
);
|
||||
return new AddEventLogResult($eventLog);
|
||||
}
|
||||
}
|
||||
36
src/EventLog/Action/Input/AddEventLogInput.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Action\Input;
|
||||
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceQuery;
|
||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/** @implements InputInterface<AddEventLogCommand> */
|
||||
class AddEventLogInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceQuery('type')]
|
||||
#[SourceRequest('type')]
|
||||
public string $type,
|
||||
|
||||
#[SourceQuery('message')]
|
||||
#[SourceRequest('message')]
|
||||
public string $message,
|
||||
|
||||
#[SourceQuery('context')]
|
||||
#[SourceRequest('context')]
|
||||
public array $context = []
|
||||
){}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new AddEventLogCommand(
|
||||
$this->type,
|
||||
$this->message,
|
||||
$this->context
|
||||
);
|
||||
}
|
||||
}
|
||||
14
src/EventLog/Action/Result/AddEventLogResult.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Action\Result;
|
||||
|
||||
use App\EventLog\Framework\Entity\EventLog;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements ResultInterface<AddEventLogResult> */
|
||||
class AddEventLogResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public EventLog $eventLog,
|
||||
) {}
|
||||
}
|
||||
28
src/EventLog/Framework/Controller/EventLogController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Framework\Controller;
|
||||
|
||||
use App\Base\Service\Broadcaster;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class EventLogController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Broadcaster $broadcaster,
|
||||
) {}
|
||||
|
||||
#[Route('/alert', name: 'app_alert')]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->broadcaster->alert(
|
||||
'Added to queue',
|
||||
'This is a testy test!'
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'Success' => 'Published'
|
||||
]);
|
||||
}
|
||||
}
|
||||
82
src/EventLog/Framework/Entity/EventLog.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Framework\Entity;
|
||||
|
||||
use App\EventLog\Framework\Repository\EventLogRepository;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EventLogRepository::class)]
|
||||
class EventLog
|
||||
{
|
||||
use TimestampableEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $type = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $message = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
private ?array $context = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'eventLogs')]
|
||||
private ?User $user = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(?string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(?string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContext(): ?array
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setContext(?array $context): self
|
||||
{
|
||||
$this->context = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
35
src/EventLog/Framework/Repository/EventLogRepository.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventLog\Framework\Repository;
|
||||
|
||||
use App\EventLog\Framework\Entity\EventLog;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<EventLog>
|
||||
*/
|
||||
class EventLogRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, EventLog::class);
|
||||
}
|
||||
|
||||
public function insert(
|
||||
User $user,
|
||||
string $type,
|
||||
string $message,
|
||||
array $context = []
|
||||
): EventLog {
|
||||
$eventLog = new EventLog()
|
||||
->setUser($user)
|
||||
->setType($type)
|
||||
->setMessage($message)
|
||||
->setContext($context);
|
||||
$this->getEntityManager()->persist($eventLog);
|
||||
$this->getEntityManager()->flush();
|
||||
return $eventLog;
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,38 @@
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\Monitor\Action\Command\AddMonitorCommand;
|
||||
use App\Monitor\Action\Result\AddMonitorResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\MonitorEvents;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<AddMonitorCommand> */
|
||||
readonly class AddMonitorHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MessageBusInterface $bus,
|
||||
private MonitorRepository $movieMonitorRepository,
|
||||
private UserRepository $userRepository,
|
||||
private TmdbClient $tmdb,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$user = $this->userRepository->find($command->userId);
|
||||
$poster = $this->getPoster($command->imdbId);
|
||||
|
||||
$monitor = (new Monitor())
|
||||
->setUser($user)
|
||||
->setPoster($poster)
|
||||
->setTmdbId($command->tmdbId)
|
||||
->setImdbId($command->imdbId)
|
||||
->setTitle($command->title)
|
||||
@@ -35,6 +44,13 @@ readonly class AddMonitorHandler implements HandlerInterface
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$user,
|
||||
MonitorEvents::MONITOR_ADDED->type(),
|
||||
MonitorEvents::MONITOR_ADDED->message(),
|
||||
(array) $monitor
|
||||
));
|
||||
|
||||
$this->movieMonitorRepository->getEntityManager()->persist($monitor);
|
||||
$this->movieMonitorRepository->getEntityManager()->flush();
|
||||
|
||||
@@ -45,4 +61,10 @@ readonly class AddMonitorHandler implements HandlerInterface
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getPoster(string $imdbId): ?string
|
||||
{
|
||||
$data = $this->tmdb->tvShowDetails($imdbId);
|
||||
return $data->poster;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use App\Monitor\Action\Command\AddMonitorCommand;
|
||||
use App\Download\DownloadEvents;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\Monitor\Action\Command\DeleteMonitorCommand;
|
||||
use App\Monitor\Action\Result\AddMonitorResult;
|
||||
use App\Monitor\Action\Result\DeleteMonitorResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
use App\Monitor\MonitorEvents;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<DeleteMonitorCommand, DeleteMonitorResult> */
|
||||
readonly class DeleteMonitorHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MessageBusInterface $bus,
|
||||
private MonitorRepository $monitorRepository,
|
||||
) {}
|
||||
|
||||
@@ -27,6 +27,13 @@ readonly class DeleteMonitorHandler implements HandlerInterface
|
||||
$this->monitorRepository->getEntityManager()->remove($monitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$monitor->getUser(),
|
||||
MonitorEvents::MONITOR_DELETED->type(),
|
||||
MonitorEvents::MONITOR_DELETED->message(),
|
||||
(array) $monitor
|
||||
));
|
||||
|
||||
return new DeleteMonitorResult(
|
||||
status: 'OK',
|
||||
result: [],
|
||||
|
||||
@@ -6,10 +6,13 @@ use App\Base\Util\EpisodeId;
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\DownloadOptionEvaluator;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\EventLog\Action\Command\AddEventLogCommand;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Monitor\MonitorEvents;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use App\User\Dto\UserPreferencesFactory;
|
||||
@@ -32,7 +35,7 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
||||
private MessageBusInterface $bus,
|
||||
private LoggerInterface $logger,
|
||||
private MonitorRepository $monitorRepository,
|
||||
private Tmdb $tmdb,
|
||||
private TmdbClient $tmdb,
|
||||
private DownloadRepository $downloadRepository,
|
||||
) {}
|
||||
|
||||
@@ -41,14 +44,33 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
||||
try {
|
||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler for ' . $monitor->getTitle() . ' season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode());
|
||||
$this->refreshData($monitor);
|
||||
|
||||
$episodeData = $this->tmdb->episodeDetails($monitor->getTmdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$monitor->getUser(),
|
||||
MonitorEvents::MONITOR_STARTED->type(),
|
||||
MonitorEvents::MONITOR_STARTED->message(),
|
||||
(array) $command
|
||||
));
|
||||
|
||||
if (null === $monitor->getAirDate() && null !== $episodeData->episodeAirDate && "" !== $episodeData->episodeAirDate) {
|
||||
$monitor->setAirDate(Carbon::parse($episodeData->episodeAirDate));
|
||||
$episodeData = $this->tmdb->tvEpisodeDetails($monitor->getTmdbId(), $monitor->getImdbId(), $monitor->getSeason(), $monitor->getEpisode());
|
||||
|
||||
if (null === $episodeData->airDate || "" === $episodeData->airDate) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode does not have an air date, skipping for now');
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'message' => 'No change',
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (Carbon::createFromTimestamp($episodeData->episodeAirDate) > Carbon::today('UTC')) {
|
||||
if (null === $monitor->getAirDate()) {
|
||||
$monitor->setAirDate(Carbon::parse($episodeData->airDate));
|
||||
}
|
||||
|
||||
if (Carbon::createFromTimestamp($episodeData->airDate) > Carbon::today('UTC')) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] ...Episode has not aired yet, skipping for now');
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
@@ -105,12 +127,25 @@ readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
||||
$this->logger->error('> [MonitorTvEpisodeHandler] ...Exception thrown: ' . $exception->getMessage());
|
||||
$this->logger->error($exception->getMessage());
|
||||
$monitor->setStatus('Active');
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$monitor->getUser(),
|
||||
MonitorEvents::MONITOR_ERROR->type(),
|
||||
MonitorEvents::MONITOR_ERROR->message() . ': ' . $exception->getMessage(),
|
||||
(array) $monitor
|
||||
));
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$this->bus->dispatch(new AddEventLogCommand(
|
||||
$monitor->getUser(),
|
||||
MonitorEvents::MONITOR_FINISHED->type(),
|
||||
MonitorEvents::MONITOR_FINISHED->message(),
|
||||
(array) $monitor
|
||||
));
|
||||
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
@@ -118,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
||||
use App\Monitor\Action\Result\MonitorTvSeasonResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Base\Util\PTN;
|
||||
@@ -26,7 +26,7 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MediaFiles $mediaFiles,
|
||||
private LoggerInterface $logger,
|
||||
private Tmdb $tmdb,
|
||||
private TmdbClient $tmdb,
|
||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||
) {}
|
||||
|
||||
@@ -50,7 +50,7 @@ readonly class MonitorTvSeasonHandler implements HandlerInterface
|
||||
|
||||
// Compare against list from TMDB
|
||||
$episodesInSeason = Map::from(
|
||||
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes[$monitor->getSeason()]
|
||||
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes[$monitor->getSeason()]
|
||||
)->rekey(fn($episode) => $episode['episode_number']);
|
||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($episodesInSeason) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||
use App\Monitor\Action\Result\MonitorTvShowResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Tmdb\Dto\TmdbEpisodeDto;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\Base\Util\PTN;
|
||||
@@ -29,89 +29,90 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||
private MediaFiles $mediaFiles,
|
||||
private LoggerInterface $logger,
|
||||
private Tmdb $tmdb,
|
||||
) {}
|
||||
private TmdbClient $tmdb,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||
$this->refreshData($monitor);
|
||||
|
||||
// Check current episodes
|
||||
$downloadedEpisodes = $this->mediaFiles
|
||||
->getEpisodes($monitor->getTitle())
|
||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
||||
->filter(fn ($episode) =>
|
||||
property_exists($episode, 'episode')
|
||||
->map(fn($episode) => (object)(new PTN())->parse($episode))
|
||||
->filter(fn($episode) => property_exists($episode, 'episode')
|
||||
&& property_exists($episode, 'season')
|
||||
&& null !== $episode->episode
|
||||
&& null !== $episode->season
|
||||
)
|
||||
;
|
||||
);
|
||||
|
||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||
|
||||
// Compare against list from TMDB
|
||||
$episodesInShow = Map::from(
|
||||
$this->tmdb->tvDetails($monitor->getTmdbId())->episodes
|
||||
)->flat(1);
|
||||
$this->tmdb->tvshowDetails($monitor->getImdbId())->episodes
|
||||
)->flat(1)
|
||||
->filter(fn(TmdbEpisodeDto $episode) => $episode->seasonNumber >= $monitor->getSeason())
|
||||
->values();
|
||||
|
||||
$this->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes for title: ' . $monitor->getTitle());
|
||||
|
||||
$episodeMonitors = [];
|
||||
if ($downloadedEpisodes->count() !== $episodesInShow->count()) {
|
||||
// Dispatch Episode commands for each missing Episode
|
||||
foreach ($episodesInShow as $episode) {
|
||||
// Only monitor future episodes
|
||||
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode['season_number'] . '" episode "' . $episode['episode_number'] . '"');
|
||||
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
||||
if (false === $episodeInFuture) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the episode is already downloaded
|
||||
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||
if (true === $episodeExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for existing monitors
|
||||
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||
if (true === $monitorExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the monitor
|
||||
$episodeMonitor = (new Monitor())
|
||||
->setParent($monitor)
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($episode['season_number'])
|
||||
->setEpisode($episode['episode_number'])
|
||||
->setAirDate($episode['air_date'] !== null && $episode['air_date'] !== "" ? Carbon::parse($episode['air_date']) : null)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$episodeMonitors[] = $episodeMonitor;
|
||||
|
||||
// Immediately run the monitor
|
||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||
$this->monitorTvEpisodeHandler->handle($command);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
||||
// Dispatch Episode commands for each missing Episode
|
||||
foreach ($episodesInShow as $episode) {
|
||||
/** @var TmdbEpisodeDto $episode */
|
||||
// Only monitor future episodes
|
||||
$this->logger->info('> [MonitorTvShowHandler] Evaluating "' . $monitor->getTitle() . '", season "' . $episode->seasonNumber . '" episode "' . $episode->episodeNumber . '"');
|
||||
$episodeInFuture = $this->episodeReleasedAfterMonitorCreated($monitor->getCreatedAt(), $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Released after monitor started? ' . (true === $episodeInFuture ? 'YES' : 'NO'));
|
||||
if (false === $episodeInFuture) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the episode is already downloaded
|
||||
$episodeExists = $this->episodeExists($episode, $downloadedEpisodes);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Episode exists? ' . (true === $episodeExists ? 'YES' : 'NO'));
|
||||
if (true === $episodeExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for existing monitors
|
||||
$monitorExists = $this->monitorExists($monitor, $episode);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Monitor exists? ' . (true === $monitorExists ? 'YES' : 'NO'));
|
||||
if (true === $monitorExists) {
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the monitor
|
||||
$episodeMonitor = (new Monitor())
|
||||
->setParent($monitor)
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($episode->seasonNumber)
|
||||
->setEpisode($episode->episodeNumber)
|
||||
->setAirDate($episode->airDate !== null && $episode->airDate !== "" ? Carbon::parse($episode->airDate) : null)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$episodeMonitors[] = $episodeMonitor;
|
||||
|
||||
// Immediately run the monitor
|
||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||
$this->monitorTvEpisodeHandler->handle($command);
|
||||
$this->logger->info('> [MonitorTvShowHandler] ...Dispatching MonitorTvEpisodeCommand');
|
||||
}
|
||||
|
||||
// Set the status to Active, so it will be re-executed.
|
||||
@@ -129,30 +130,43 @@ readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
);
|
||||
}
|
||||
|
||||
private function episodeReleasedAfterMonitorCreated(string|DateTimeImmutable $monitorStartDate, array $episodeInShow): bool
|
||||
{
|
||||
private function episodeReleasedAfterMonitorCreated(
|
||||
string|DateTimeImmutable $monitorStartDate,
|
||||
TmdbEpisodeDto $episodeInShow
|
||||
): bool {
|
||||
$monitorStartDate = Carbon::parse($monitorStartDate)->setTime(0, 0);
|
||||
$episodeAirDate = Carbon::parse($episodeInShow['air_date']);
|
||||
$episodeAirDate = Carbon::parse($episodeInShow->airDate);
|
||||
return $episodeAirDate >= $monitorStartDate;
|
||||
}
|
||||
|
||||
private function episodeExists(array $episodeInShow, Map $downloadedEpisodes): bool
|
||||
private function episodeExists(TmdbEpisodeDto $episodeInShow, Map $downloadedEpisodes): bool
|
||||
{
|
||||
return $downloadedEpisodes->filter(
|
||||
fn (object $episode) => $episode->episode === $episodeInShow['episode_number']
|
||||
&& $episode->season === $episodeInShow['season_number']
|
||||
fn(object $episode) => $episode->episode === $episodeInShow->episodeNumber
|
||||
&& $episode->season === $episodeInShow->seasonNumber
|
||||
)->count() > 0;
|
||||
}
|
||||
|
||||
private function monitorExists(Monitor $monitor, array $episode): bool
|
||||
private function monitorExists(Monitor $monitor, TmdbEpisodeDto $episode): bool
|
||||
{
|
||||
return $this->monitorRepository->findOneBy([
|
||||
'imdbId' => $monitor->getImdbId(),
|
||||
'title' => $monitor->getTitle(),
|
||||
'monitorType' => 'tvepisode',
|
||||
'season' => $episode['season_number'],
|
||||
'episode' => $episode['episode_number'],
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Dto;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class UpcomingEpisode
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $airDate {
|
||||
get => Carbon::parse($this->airDate)->format('m/d/Y');
|
||||
},
|
||||
public string $episodeTitle,
|
||||
public int $episodeNumber,
|
||||
) {}
|
||||
}
|
||||
@@ -71,13 +71,34 @@ class ApiController extends AbstractController
|
||||
#[Route('/api/monitor/upcoming-episodes', name: 'api.monitor.upcoming-episodes', methods: ['GET'])]
|
||||
public function upcomingEpisodes(MonitorRepository $repository): Response
|
||||
{
|
||||
$colors = [
|
||||
'blue' => '#007bff',
|
||||
'indigo' => '#6610f2',
|
||||
'purple' => '#6f42c1',
|
||||
'pink' => '#e83e8c',
|
||||
'red' => '#dc3545',
|
||||
'orange' => '#fd7e14',
|
||||
'yellow' => '#ffc107',
|
||||
'green' => '#28a745',
|
||||
'teal' => '#20c997',
|
||||
'cyan' => '#17a2b8',
|
||||
];
|
||||
|
||||
$eventColors = [];
|
||||
$monitors = $repository->whereAirDateNotNull();
|
||||
$monitors = Map::from($monitors)->map(function ($monitor) {
|
||||
$monitors = Map::from($monitors)->map(function ($monitor) use (&$eventColors, $colors) {
|
||||
if (!array_key_exists($monitor->getImdbId(), $eventColors)) {
|
||||
$eventColors[$monitor->getImdbId()] = $colors[array_rand($colors)];
|
||||
}
|
||||
return [
|
||||
'id' => $monitor->getId(),
|
||||
'title' => $monitor->getTitle() . ' (S' . str_pad($monitor->getSeason(), 2, '0', STR_PAD_LEFT) . 'E' . str_pad($monitor->getEpisode(), 2, '0', STR_PAD_LEFT) . ')',
|
||||
'start' => $monitor->getAirDate()->format('Y-m-d H:i:s'),
|
||||
'allDay' => true
|
||||
'groupId' => $monitor->getImdbId(),
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $eventColors[$monitor->getImdbId()],
|
||||
'borderColor' => $eventColors[$monitor->getImdbId()],
|
||||
'attachment' => $monitor->getPoster(),
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
46
src/Monitor/Framework/Controller/CalendarController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||
use Spatie\IcalendarGenerator\Components\Event;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
#[Route('/monitors/ical/{email:user}/upcoming-episodes.ics', name: 'app.monitors.ical')]
|
||||
public function icalAction(MonitorRepository $monitorRepository, User $user)
|
||||
{
|
||||
if (false === $user->hasICalEnabled()) {
|
||||
return new Response('Calendar not found.', 404);
|
||||
}
|
||||
|
||||
$calendar = Calendar::create()
|
||||
->name('Upcoming Episodes')
|
||||
->refreshInterval(10);
|
||||
|
||||
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||
$calendar->event(Map::from($monitors)->map(function (Monitor $monitor) {
|
||||
$event = new Event($monitor->getTitle())
|
||||
->startsAt($monitor->getAirDate())
|
||||
->fullDay();
|
||||
if (null !== $monitor->getPoster()) {
|
||||
$event->attachment($monitor->getPoster());
|
||||
}
|
||||
return $event;
|
||||
})->toArray());
|
||||
|
||||
return new Response($calendar->get(), 200, [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Content-Disposition' => 'inline; filename="upcoming-episodes.ics"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,17 @@
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
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\DeleteMonitorHandler;
|
||||
use App\Monitor\Action\Input\AddMonitorInput;
|
||||
use App\Monitor\Action\Input\DeleteMonitorInput;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
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\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -23,7 +29,7 @@ class WebController extends AbstractController
|
||||
private readonly Environment $renderer,
|
||||
) {}
|
||||
|
||||
#[Route('/monitors', name: 'app_monitors', methods: ['GET'])]
|
||||
#[Route('/monitors', name: 'app.monitors', methods: ['GET'])]
|
||||
public function addMonitor()
|
||||
{
|
||||
return $this->render('monitor/index.html.twig');
|
||||
@@ -34,4 +40,29 @@ class WebController extends AbstractController
|
||||
{
|
||||
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)]
|
||||
private ?int $searchCount = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?string $poster = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $onlyFuture = true;
|
||||
|
||||
@@ -68,7 +71,7 @@ class Monitor
|
||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
private ?self $parent = null;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
|
||||
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent', cascade: ['remove'])]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct()
|
||||
@@ -230,6 +233,17 @@ class Monitor
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPoster(): ?string
|
||||
{
|
||||
return $this->poster;
|
||||
}
|
||||
|
||||
public function setPoster(?string $poster): ?self
|
||||
{
|
||||
$this->poster = $poster;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
|
||||
@@ -41,4 +41,83 @@ class MonitorRepository extends ServiceEntityRepository
|
||||
->getQuery();
|
||||
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()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Monitor/MonitorEvents.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor;
|
||||
|
||||
enum MonitorEvents
|
||||
{
|
||||
case MONITOR_ADDED;
|
||||
case MONITOR_STARTED;
|
||||
case MONITOR_FINISHED;
|
||||
case MONITOR_DELETED;
|
||||
case MONITOR_ERROR;
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MONITOR_ADDED => 'monitor_added',
|
||||
self::MONITOR_STARTED => 'monitor_started',
|
||||
self::MONITOR_FINISHED => 'monitor_finished',
|
||||
self::MONITOR_DELETED => 'monitor_deleted',
|
||||
self::MONITOR_ERROR => 'monitor_error',
|
||||
};
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MONITOR_ADDED => 'A new monitor has been added.',
|
||||
self::MONITOR_STARTED => 'A monitor has started.',
|
||||
self::MONITOR_FINISHED => 'A monitor has finished.',
|
||||
self::MONITOR_DELETED => 'A monitor has been deleted',
|
||||
self::MONITOR_ERROR => 'A monitor has encountered an error.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Search\Action\Handler;
|
||||
|
||||
use App\Base\Enum\MediaType;
|
||||
use App\Search\Action\Command\GetMediaInfoCommand;
|
||||
use App\Search\Action\Result\GetMediaInfoResult;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
@@ -13,13 +15,30 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
class GetMediaInfoHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Tmdb $tmdb,
|
||||
private readonly TmdbClient $tmdb,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||
$handlers = [
|
||||
MediaType::Movie->value => 'getMovieDetails',
|
||||
MediaType::TvShow->value => 'getTvshowDetails',
|
||||
];
|
||||
$handler = $handlers[$command->mediaType];
|
||||
$media = $this->$handler($command);
|
||||
$relatedMedia = $this->tmdb->relatedMedia($media->tmdbId, $command->mediaType);
|
||||
|
||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
||||
return new GetMediaInfoResult($media, $relatedMedia, $command->season, $command->episode);
|
||||
}
|
||||
|
||||
private function getMovieDetails(CommandInterface $command): TmdbResult
|
||||
{
|
||||
return $this->tmdb->movieDetails($command->imdbId);
|
||||
}
|
||||
|
||||
private function getTvshowDetails(CommandInterface $command): TmdbResult
|
||||
{
|
||||
$media = $this->tmdb->tvshowDetails($command->imdbId);
|
||||
return $media;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Search\Action\Handler;
|
||||
|
||||
use App\Base\Util\ImdbMatcher;
|
||||
use App\Search\Action\Result\RedirectToMediaResult;
|
||||
use App\Search\Action\Result\SearchResult;
|
||||
use App\Tmdb\Tmdb;
|
||||
use App\Tmdb\TmdbClient;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
@@ -14,13 +14,13 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
class SearchHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Tmdb $tmdb,
|
||||
private TmdbClient $tmdb,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
if (ImdbMatcher::isMatch($command->term)) {
|
||||
$result = $this->tmdb->findByImdbId($command->term);
|
||||
$result = $this->tmdb->search($command->term);
|
||||
if ($result instanceof TmdbResult) {
|
||||
return new RedirectToMediaResult(
|
||||
imdbId: $result->imdbId,
|
||||
mediaType: $result->mediaType,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Search\Action\Result;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
@@ -10,6 +11,7 @@ class GetMediaInfoResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public TmdbResult $media,
|
||||
public Map|array $relatedMedia,
|
||||
public ?int $season,
|
||||
public ?int $episode,
|
||||
) {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Search\Action\Result;
|
||||
|
||||
use Aimeos\Map;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements ResultInterface<SearchResult> */
|
||||
@@ -9,6 +10,6 @@ class SearchResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $term = "",
|
||||
public array $results = []
|
||||
public Map|array $results = [],
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ use App\Search\Action\Handler\SearchHandler;
|
||||
use App\Search\Action\Input\GetMediaInfoInput;
|
||||
use App\Search\Action\Input\SearchInput;
|
||||
use App\Search\Action\Result\RedirectToMediaResult;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
19
src/Tmdb/Dto/CastMemberDto.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tmdb\Dto;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||
|
||||
class CastMemberDto
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
#[SerializedPath('[name]')]
|
||||
public string $name,
|
||||
) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
18
src/Tmdb/Dto/CrewMemberDto.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tmdb\Dto;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||
|
||||
class CrewMemberDto
|
||||
{
|
||||
public function __construct(
|
||||
#[SerializedPath('[name]')]
|
||||
public string $name,
|
||||
) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
18
src/Tmdb/Dto/GenreDto.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tmdb\Dto;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedPath;
|
||||
|
||||
class GenreDto
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||