Compare commits
51 Commits
dev-landin
...
dev-monito
| Author | SHA1 | Date | |
|---|---|---|---|
| 4506306377 | |||
| 4287b52bd4 | |||
| 3724bcbb16 | |||
| 6c2cd7510f | |||
| 98bf8d2880 | |||
| 20ade478b1 | |||
| 4eed5fef78 | |||
| 5ff9842eaa | |||
| b93da3df1d | |||
| fe0ab2ef5a | |||
| 25ff3e726d | |||
| 527adb73c1 | |||
| 9166b4bbc8 | |||
| 5688b3a0df | |||
| babcb00440 | |||
| 993b34d668 | |||
| 73b3e5179a | |||
| d3176baff2 | |||
| cc77cccf0b | |||
| b0c10a028a | |||
| 12bf90a2b4 | |||
| 687b5ed873 | |||
| e5f0f358b7 | |||
| fd84648100 | |||
| f3285ba60c | |||
| 4f6f8f43f1 | |||
| b23d8a2ba3 | |||
| bfd5f53d67 | |||
| f10168a1a7 | |||
| 8970ca0f8f | |||
| 994bd775ea | |||
| d0e7941809 | |||
| a28f0d9369 | |||
| 964cdca151 | |||
| b59069551a | |||
| 3971cf3260 | |||
| 8a1a89f17d | |||
| c3eaf109e3 | |||
| 0225bead60 | |||
| 1b1feaebec | |||
| 7045116b56 | |||
| 883442225f | |||
| c664e9fbca | |||
| 5d257e4404 | |||
| a6dc4f0b03 | |||
| 3c6e41af94 | |||
| 3e081df01c | |||
| 3384720c09 | |||
| c32ff2e464 | |||
| 6138c94d7a | |||
| a1a38cb74c |
1
.env.dev
1
.env.dev
@@ -5,3 +5,4 @@ DOWNLOAD_DIR=./movies
|
||||
MERCURE_URL=http://mercure/.well-known/mercure
|
||||
MERCURE_PUBLIC_URL=https://dev.caldwell.digital/hub/.well-known/mercure
|
||||
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||
MONITOR_FREQUENCY="* * * * *"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
APP_ENV=%%app_env%%
|
||||
APP_SECRET="%%app_secret%%"
|
||||
DATABASE_URL="%%db_url%%"
|
||||
DOWNLOAD_DIR=%%download_dir%%
|
||||
REAL_DEBRID_KEY="%%rd_key%%"
|
||||
@@ -7,3 +9,4 @@ MERCURE_PUBLIC_URL=%%mercure_public_url%%
|
||||
MERCURE_JWT_SECRET="%%mercure_jwt_secret%%"
|
||||
JELLYFIN_URL=%%jellyfin_url%%
|
||||
JELLYFIN_TOKEN=%%jellyfin_token%%
|
||||
REDIS_HOST="%%redis_host%%"
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
FROM registry.caldwell.digital/library/php:8.4-apache
|
||||
|
||||
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
||||
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
@@ -2,5 +2,4 @@ FROM registry.caldwell.digital/library/php:8.4-apache
|
||||
|
||||
COPY --chown=www-data:www-data . /var/www
|
||||
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
||||
|
||||
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
@@ -8,3 +8,13 @@ import './bootstrap.js';
|
||||
import './styles/app.css';
|
||||
|
||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||
|
||||
let alert = document.querySelector('.alert');
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
if (document.contains(alert)) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
"@symfony/ux-live-component/dist/live.min.css": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
|
||||
18
assets/controllers/alert_controller.js
Normal file
18
assets/controllers/alert_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
let timer = setTimeout(() => {
|
||||
this.element.remove();
|
||||
},
|
||||
"3000"
|
||||
);
|
||||
|
||||
this.element.addEventListener('mouseout', () => timer = setTimeout(() => {
|
||||
this.element.remove();
|
||||
},
|
||||
"3000"
|
||||
));
|
||||
this.element.addEventListener('mouseover', () => clearTimeout(timer));
|
||||
}
|
||||
}
|
||||
93
assets/controllers/monitor_button_controller.js
Normal file
93
assets/controllers/monitor_button_controller.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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 = ['button', 'options']
|
||||
static outlets = ['result-filter']
|
||||
static values = {
|
||||
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)
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.optionsTarget.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
async monitorSeries() {
|
||||
await this.makeMonitor({
|
||||
tmdbId: this.tmdbIdValue,
|
||||
imdbId: this.imdbIdValue,
|
||||
title: this.titleValue,
|
||||
monitorType: 'tvshows',
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
24
assets/controllers/monitor_controller.js
Normal file
24
assets/controllers/monitor_controller.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
title: String,
|
||||
tmdbId: String,
|
||||
imdbId: String,
|
||||
mediaType: String,
|
||||
}
|
||||
|
||||
addMovieMonitor() {
|
||||
console.log(`/monitor/movies/${this.tmdbIdValue}/${this.imdbIdValue}/${encodeURI(this.titleValue)}`)
|
||||
fetch(`/monitor/movies/${this.tmdbIdValue}/${this.imdbIdValue}/${encodeURI(this.titleValue)}`)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log(json)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import { Controller } from '@hotwired/stimulus';
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
H264_CODECS = ['h264', 'h.264', 'x264']
|
||||
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
||||
|
||||
static values = {
|
||||
title: String,
|
||||
tmdbId: String,
|
||||
@@ -15,13 +18,15 @@ export default class extends Controller {
|
||||
static targets = ['list']
|
||||
|
||||
options = []
|
||||
optionsLoaded = false
|
||||
|
||||
async connect() {
|
||||
await this.setOptions();
|
||||
}
|
||||
|
||||
async setOptions() {
|
||||
if (this.options.length === 0) {
|
||||
if (false === this.optionsLoaded) {
|
||||
this.optionsLoaded = true;
|
||||
await fetch(`/torrentio/movies/${this.tmdbIdValue}/${this.imdbIdValue}`)
|
||||
.then(res => res.text())
|
||||
.then(response => {
|
||||
@@ -37,7 +42,54 @@ export default class extends Controller {
|
||||
return true;
|
||||
}
|
||||
|
||||
listTargetConnected(target) {
|
||||
// console.log(target);
|
||||
async filter(activeFilter) {
|
||||
let firstIncluded = true;
|
||||
let count = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
this.options.forEach((option) => {
|
||||
const props = {
|
||||
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||
"codec": option.querySelector('#codec').textContent.trim(),
|
||||
"provider": option.querySelector('#provider').textContent.trim(),
|
||||
"languages": JSON.parse(option.dataset['languages']),
|
||||
}
|
||||
|
||||
let include = true;
|
||||
option.classList.remove('hidden');
|
||||
option.querySelector('input[type="checkbox"]').checked = false;
|
||||
|
||||
for (let [key, value] of Object.entries(activeFilter)) {
|
||||
if (value === "" || key === "season") {
|
||||
continue;
|
||||
}
|
||||
if (key === "codec" && value === "h264") {
|
||||
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "codec" && value === "h265") {
|
||||
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "language") {
|
||||
if (!props["languages"].includes(value)) {
|
||||
include = false;
|
||||
}
|
||||
} else if (props[key] !== value) {
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === include) {
|
||||
option.classList.add('hidden');
|
||||
} else if (true === firstIncluded) {
|
||||
count = 1;
|
||||
selectedCount = selectedCount + 1;
|
||||
option.querySelector('input[type="checkbox"]').checked = true;
|
||||
firstIncluded = false;
|
||||
} else {
|
||||
count = count + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
25
assets/controllers/navbar_controller.js
Normal file
25
assets/controllers/navbar_controller.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
inactiveStyles = "block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-orange-500 hover:bg-opacity-80";
|
||||
activeStyles = "block rounded-lg bg-orange-500 hover:bg-opacity-70 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-80 px-4 py-2 text-sm font-medium text-gray-50";
|
||||
|
||||
connect() {
|
||||
console.log(window.location.pathname);
|
||||
this.element.querySelectorAll('a:not(.nav-foot)').forEach(link => {
|
||||
link.className = this.inactiveStyles;
|
||||
if (window.location.pathname === (new URL(link.href)).pathname || window.location.pathname.startsWith( (new URL(link.href)).pathname + '/' ) ) {
|
||||
link.className = this.activeStyles;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setActive() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,17 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
static outlets = ['movie-results', 'tv-results']
|
||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'selectAll']
|
||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'selectAll', 'downloadSelected']
|
||||
static values = {
|
||||
'media-type': String,
|
||||
'episodes': Array,
|
||||
}
|
||||
|
||||
connect() {
|
||||
async connect() {
|
||||
if (this.mediaTypeValue === "tvshows") {
|
||||
this.activeFilter['season'] = 1;
|
||||
}
|
||||
await this.filter();
|
||||
}
|
||||
|
||||
async movieResultsOutletConnected(outlet) {
|
||||
@@ -49,6 +50,7 @@ export default class extends Controller {
|
||||
this.addLanguages(option, option.dataset);
|
||||
this.addProviders(option, option.dataset);
|
||||
})
|
||||
await this.filter();
|
||||
}
|
||||
|
||||
addLanguages(option, props) {
|
||||
@@ -59,9 +61,22 @@ export default class extends Controller {
|
||||
}
|
||||
});
|
||||
|
||||
this.languageTarget.innerHTML = '<option value="">n/a</option>';
|
||||
const preferred = this.languageTarget.dataset.preferred;
|
||||
if (preferred) {
|
||||
this.languageTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
|
||||
this.languageTarget.innerHTML += '<option value="">n/a</option>';
|
||||
} else {
|
||||
this.languageTarget.innerHTML = '<option value="">n/a</option>';
|
||||
}
|
||||
|
||||
this.languageTarget.innerHTML += this.languages.sort()
|
||||
.map((language) => '<option value="'+language+'">'+language+'</option>')
|
||||
.map((language) => {
|
||||
const preferred = this.languageTarget.dataset.preferred;
|
||||
if (preferred === language) {
|
||||
return;
|
||||
}
|
||||
return '<option value="'+language+'">'+language+'</option>';
|
||||
})
|
||||
.join();
|
||||
}
|
||||
|
||||
@@ -70,9 +85,22 @@ export default class extends Controller {
|
||||
this.providers.push(props['provider']);
|
||||
}
|
||||
|
||||
this.providerTarget.innerHTML = '<option value="">n/a</option>';
|
||||
const preferred = this.providerTarget.dataset.preferred;
|
||||
if (preferred) {
|
||||
this.providerTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
|
||||
this.providerTarget.innerHTML += '<option value="">n/a</option>';
|
||||
} else {
|
||||
this.providerTarget.innerHTML = '<option value="">n/a</option>';
|
||||
}
|
||||
|
||||
this.providerTarget.innerHTML += this.providers.sort()
|
||||
.map((provider) => '<option value="'+provider+'">'+provider+'</option>')
|
||||
.map((provider) => {
|
||||
const preferred = this.languageTarget.dataset.preferred;
|
||||
if (preferred === provider) {
|
||||
return;
|
||||
}
|
||||
return '<option value="' + provider + '">' + provider + '</option>'
|
||||
})
|
||||
.join();
|
||||
|
||||
}
|
||||
@@ -90,87 +118,33 @@ export default class extends Controller {
|
||||
|
||||
if ("movies" === this.mediaTypeValue) {
|
||||
results = this.movieResultsOutlets;
|
||||
await results.forEach((list) => list.filter(this.activeFilter));
|
||||
|
||||
} else if ("tvshows" === this.mediaTypeValue) {
|
||||
results = this.tvResultsOutlets;
|
||||
this.activeFilter.season = this.seasonTarget.value;
|
||||
await results.forEach((list) => list.filter(this.activeFilter, currentSeason, this.seasonTarget.value));
|
||||
}
|
||||
|
||||
const filterOperation = async (resultList, currentSeason) => {
|
||||
if ("tvshows" === this.mediaTypeValue && currentSeason !== this.activeFilter['season']) {
|
||||
if (resultList.seasonValue === this.seasonTarget.value) {
|
||||
await resultList.setActive();
|
||||
} else {
|
||||
resultList.setInActive();
|
||||
}
|
||||
}
|
||||
|
||||
if (false === resultList.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let firstIncluded = true;
|
||||
let count = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
resultList.options.forEach((option) => {
|
||||
const props = {
|
||||
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||
"codec": option.querySelector('#codec').textContent.trim(),
|
||||
"provider": option.querySelector('#provider').textContent.trim(),
|
||||
"languages": JSON.parse(option.dataset['languages']),
|
||||
}
|
||||
|
||||
let include = true;
|
||||
option.classList.remove('hidden');
|
||||
|
||||
for (let [key, value] of Object.entries(this.activeFilter)) {
|
||||
if (value === "" || key === "season") {
|
||||
continue;
|
||||
}
|
||||
if (key === "codec" && value === "h264") {
|
||||
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "codec" && value === "h265") {
|
||||
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "language") {
|
||||
if (!props["languages"].includes(value)) {
|
||||
include = false;
|
||||
}
|
||||
} else if (props[key] !== value) {
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === include) {
|
||||
option.classList.add('hidden');
|
||||
} else if (true === firstIncluded) {
|
||||
count = 1;
|
||||
selectedCount = selectedCount + 1;
|
||||
option.querySelector('input[type="checkbox"]').checked = true;
|
||||
firstIncluded = false;
|
||||
} else {
|
||||
count = count + 1;
|
||||
}
|
||||
|
||||
if ("tvshows" === this.mediaTypeValue) {
|
||||
resultList.countTarget.innerText = count;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
await results.forEach((list) => filterOperation(list, currentSeason));
|
||||
}
|
||||
|
||||
uncheckSelectAllBtn() {
|
||||
console.log('hurr');
|
||||
this.selectAllTarget.checked = false;
|
||||
}
|
||||
|
||||
selectAllEpisodes() {
|
||||
this.tvResultsOutlets.forEach((episode) => episode.selectEpisodeForDownload());
|
||||
this.tvResultsOutlets.forEach((episode) => {
|
||||
if (episode.isActive()) {
|
||||
episode.selectEpisodeForDownload()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadSelectedEpisodes() {
|
||||
this.tvResultsOutlets.forEach(episode => {
|
||||
if (episode.isActive() && episode.isSelected()) {
|
||||
episode.download();
|
||||
}
|
||||
});
|
||||
this.selectAllTarget.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import { Controller } from '@hotwired/stimulus';
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
H264_CODECS = ['h264', 'h.264', 'x264']
|
||||
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
||||
|
||||
static values = {
|
||||
title: String,
|
||||
tmdbId: String,
|
||||
@@ -26,7 +29,8 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
async setOptions() {
|
||||
if (true === this.activeValue) {
|
||||
if (true === this.activeValue && this.optionsLoaded === false) {
|
||||
this.optionsLoaded = true;
|
||||
await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||
.then(res => res.text())
|
||||
.then(response => {
|
||||
@@ -38,12 +42,18 @@ export default class extends Controller {
|
||||
} else {
|
||||
this.episodeSelectorTarget.disabled = true;
|
||||
}
|
||||
this.optionsLoaded = true;
|
||||
this.loadingIconOutlet.increaseCount();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// async clearCache() {
|
||||
// await fetch(`/torrentio/tvshows/clear/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||
// .then(res => res.text())
|
||||
// .then(response => {});
|
||||
// }
|
||||
|
||||
async setActive() {
|
||||
this.activeValue = true;
|
||||
this.element.classList.remove('hidden');
|
||||
@@ -52,9 +62,11 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
setInActive() {
|
||||
async setInActive() {
|
||||
this.activeValue = false;
|
||||
// if (true === this.hasEpisodeSelectorTarget()) {
|
||||
this.episodeSelectorTarget.checked = false;
|
||||
// }
|
||||
this.element.classList.add('hidden');
|
||||
}
|
||||
|
||||
@@ -62,6 +74,10 @@ export default class extends Controller {
|
||||
return this.activeValue;
|
||||
}
|
||||
|
||||
isSelected() {
|
||||
return this.episodeSelectorTarget.checked;
|
||||
}
|
||||
|
||||
selectEpisodeForDownload() {
|
||||
if (true === this.isActive() && this.episodeSelectorTarget.disabled === false) {
|
||||
this.episodeSelectorTarget.checked = !this.episodeSelectorTarget.checked;
|
||||
@@ -71,4 +87,82 @@ export default class extends Controller {
|
||||
toggleList() {
|
||||
this.listTarget.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
download() {
|
||||
this.options.forEach(option => {
|
||||
const optionSelector = option.querySelector('input[type="checkbox"]');
|
||||
if (true === optionSelector.checked) {
|
||||
const downloadBtn = option.querySelector('button.download-btn');
|
||||
const downloadBtnController = this.application.getControllerForElementAndIdentifier(downloadBtn, 'download-button');
|
||||
downloadBtnController.download();
|
||||
optionSelector.checked = false;
|
||||
this.episodeSelectorTarget.checked = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async filter(activeFilter, currentSeason, newSeason) {
|
||||
if (currentSeason !== activeFilter['season']) {
|
||||
if (this.seasonValue === newSeason) {
|
||||
await this.setActive();
|
||||
} else {
|
||||
await this.setInActive();
|
||||
}
|
||||
}
|
||||
|
||||
if (false === this.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let firstIncluded = true;
|
||||
let count = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
this.options.forEach((option) => {
|
||||
const props = {
|
||||
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||
"codec": option.querySelector('#codec').textContent.trim(),
|
||||
"provider": option.querySelector('#provider').textContent.trim(),
|
||||
"languages": JSON.parse(option.dataset['languages']),
|
||||
}
|
||||
|
||||
let include = true;
|
||||
option.classList.remove('hidden');
|
||||
option.querySelector('input[type="checkbox"]').checked = false;
|
||||
|
||||
for (let [key, value] of Object.entries(activeFilter)) {
|
||||
if (value === "" || key === "season") {
|
||||
continue;
|
||||
}
|
||||
if (key === "codec" && value === "h264") {
|
||||
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "codec" && value === "h265") {
|
||||
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
|
||||
include = false;
|
||||
}
|
||||
} else if (key === "language") {
|
||||
if (!props["languages"].includes(value)) {
|
||||
include = false;
|
||||
}
|
||||
} else if (props[key] !== value) {
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === include) {
|
||||
option.classList.add('hidden');
|
||||
} else if (true === firstIncluded) {
|
||||
count = 1;
|
||||
selectedCount = selectedCount + 1;
|
||||
option.querySelector('input[type="checkbox"]').checked = true;
|
||||
firstIncluded = false;
|
||||
} else {
|
||||
count = count + 1;
|
||||
}
|
||||
|
||||
this.countTarget.innerText = count;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1
assets/icons/hugeicons/loading-01.svg
Normal file
1
assets/icons/hugeicons/loading-01.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M17.201 2H6.8c-1.458 0-2.737.985-2.795 2.404c-.074 1.785 1.182 2.97 2.5 4.083c1.825 1.54 2.737 2.31 2.832 3.284q.023.229 0 .458c-.095.975-1.007 1.744-2.832 3.284c-1.355 1.143-2.578 2.207-2.5 4.083C4.062 21.016 5.34 22 6.799 22H17.2c1.458 0 2.737-.985 2.796-2.404c.046-1.13-.373-2.254-1.262-3.036c-.405-.357-.826-.698-1.24-1.047c-1.824-1.54-2.736-2.31-2.831-3.284a2.3 2.3 0 0 1 0-.458c.095-.975 1.008-1.744 2.832-3.284c1.34-1.131 2.577-2.229 2.5-4.083C19.939 2.984 18.66 2 17.202 2"/><path d="M9 21.638c0-.442 0-.663.088-.856a1 1 0 0 1 .046-.09c.107-.183.288-.312.65-.571c1.006-.719 1.51-1.078 2.081-1.116q.135-.009.27 0c.572.038 1.075.397 2.08 1.116c.363.259.544.388.651.571q.026.045.046.09c.088.193.088.414.088.856V22H9z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 928 B |
1
assets/icons/material-symbols/logout.svg
Normal file
1
assets/icons/material-symbols/logout.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h7v2H5v14h7v2zm11-4l-1.375-1.45l2.55-2.55H9v-2h8.175l-2.55-2.55L16 7l5 5z"/></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
1
assets/icons/ri/user-line.svg
Normal file
1
assets/icons/ri/user-line.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -4,6 +4,11 @@
|
||||
DocumentRoot /var/www/public
|
||||
DirectoryIndex /index.php
|
||||
|
||||
<LocationMatch "/hub/">
|
||||
ProxyPass http://mercure:80/
|
||||
ProxyPassReverse http://mercure:80/
|
||||
</LocationMatch>
|
||||
|
||||
<Directory /var/www/public>
|
||||
AllowOverride None
|
||||
Order Allow,Deny
|
||||
|
||||
37
build.xml
37
build.xml
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="Caldwell Digital Symfony Template" default="build">
|
||||
<!-- build dev for dev envs -->
|
||||
<target name="build" depends="setEnv,composer,compileAssets" />
|
||||
<target name="build" depends="setEnv,composer,compileAssets,migrateDb,clearCache" />
|
||||
|
||||
<target name="composer" description="Run composer">
|
||||
<exec executable="composer">
|
||||
@@ -9,6 +9,11 @@
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="setEnv" description="Set the database configuration">
|
||||
<copy file="${project.basedir}/.env.properties" tofile="${project.basedir}/.env.local" overwrite="true">
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
<target name="compileAssets" description="Run composer">
|
||||
<exec executable="php">
|
||||
<arg value="bin/console" />
|
||||
@@ -20,21 +25,19 @@
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="setEnv" description="Set the database configuration">
|
||||
<copy file="${project.basedir}/.env.dist" tofile="${project.basedir}/.env.local" overwrite="true">
|
||||
<filterchain>
|
||||
<replacetokens begintoken="%%" endtoken="%%">
|
||||
<token key="db_url" value="${DATABASE_URL}" />
|
||||
<token key="download_dir" value="${DOWNLOAD_DIR}" />
|
||||
<token key="rd_key" value="${REAL_DEBRID_KEY}" />
|
||||
<token key="tmdb_api" value="${TMDB_API}" />
|
||||
<token key="mercure_url" value="${MERCURE_URL}" />
|
||||
<token key="mercure_public_url" value="${MERCURE_PUBLIC_URL}" />
|
||||
<token key="mercure_jwt_secret" value="${MERCURE_JWT_SECRET}" />
|
||||
<token key="jellyfin_url" value="${JELLYFIN_URL}" />
|
||||
<token key="jellyfin_token" value="${JELLYFIN_TOKEN}" />
|
||||
</replacetokens>
|
||||
</filterchain>
|
||||
</copy>
|
||||
<target name="migrateDb" description="Migrate the database">
|
||||
<exec executable="php">
|
||||
<arg value="bin/console" />
|
||||
<arg value="--no-interaction" />
|
||||
<arg value="doctrine:migrations:migrate" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="clearCache" description="Clear the application cache">
|
||||
<exec executable="php">
|
||||
<arg value="bin/console" />
|
||||
<arg value="cache:pool:clear" />
|
||||
<arg value="cache.app" />
|
||||
</exec>
|
||||
</target>
|
||||
</project>
|
||||
|
||||
37
compose.yml
37
compose.yml
@@ -12,6 +12,13 @@ services:
|
||||
- $PWD/bash/caddy:/etc/caddy
|
||||
- $PWD/bash/certs:/etc/ssl
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --maxmemory 512MB
|
||||
restart: unless-stopped
|
||||
|
||||
php:
|
||||
build: .
|
||||
volumes:
|
||||
@@ -22,7 +29,32 @@ services:
|
||||
volumes:
|
||||
- ./:/var/www
|
||||
- ./var/download:/var/download
|
||||
command: php ./bin/console messenger:consume async -v --time-limit=3600 --limit=10
|
||||
command: php ./bin/console messenger:consume async -vv --time-limit=3600
|
||||
|
||||
scheduler:
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/var/www
|
||||
- ./var/download:/var/download
|
||||
command: php ./bin/console messenger:consume scheduler_monitor -vv --time-limit=3600
|
||||
|
||||
mercure:
|
||||
image: dunglas/mercure
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
SERVER_NAME: ':80'
|
||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_EXTRA_DIRECTIVES: |
|
||||
cors_origins *
|
||||
anonymous
|
||||
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
|
||||
volumes:
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
|
||||
|
||||
database:
|
||||
image: mariadb:10.11.2
|
||||
@@ -43,3 +75,6 @@ services:
|
||||
|
||||
volumes:
|
||||
mysql:
|
||||
mercure_data:
|
||||
mercure_config:
|
||||
redis_data:
|
||||
|
||||
@@ -8,28 +8,37 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"1tomany/rich-bundle": "^1.8",
|
||||
"aimeos/map": "^3.12",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.14",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||
"doctrine/orm": "^3.3",
|
||||
"dragonmantank/cron-expression": "^3.4",
|
||||
"nesbot/carbon": "^3.9",
|
||||
"nihilarr/parse-torrent-name": "^0.0.1",
|
||||
"nyholm/psr7": "*",
|
||||
"p3k/emoji-detector": "^1.2",
|
||||
"php-tmdb/api": "^4.1",
|
||||
"predis/predis": "^2.4",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/doctrine-messenger": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/filesystem": "7.2.*",
|
||||
"symfony/finder": "7.2.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/mercure-bundle": "^0.3.9",
|
||||
"symfony/messenger": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/scheduler": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/stimulus-bundle": "^2.24",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/ux-icons": "^2.24",
|
||||
"symfony/ux-live-component": "^2.24",
|
||||
"symfony/ux-turbo": "^2.24",
|
||||
"symfony/ux-twig-component": "^2.24",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfonycasts/tailwind-bundle": "^0.10.0",
|
||||
@@ -89,6 +98,9 @@
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/maker-bundle": "^1.62"
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
}
|
||||
}
|
||||
|
||||
1546
composer.lock
generated
1546
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,4 +14,7 @@ return [
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
];
|
||||
|
||||
@@ -8,8 +8,8 @@ framework:
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
app: cache.adapter.redis
|
||||
default_redis_provider: '%env(REDIS_HOST)%'
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
@@ -18,18 +18,24 @@ doctrine:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
# App:
|
||||
# type: attribute
|
||||
# is_bundle: false
|
||||
# dir: '%kernel.project_dir%/src/Entity'
|
||||
# prefix: 'App\Entity'
|
||||
# alias: App
|
||||
Download:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Download/Framework/Entity'
|
||||
prefix: 'App\Download\Framework\Entity'
|
||||
alias: Download
|
||||
User:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/User/Framework/Entity'
|
||||
prefix: 'App\User\Framework\Entity'
|
||||
alias: User
|
||||
Monitor:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Monitor/Framework/Entity'
|
||||
prefix: 'App\Monitor\Framework\Entity'
|
||||
alias: Download
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
serializer:
|
||||
default_context:
|
||||
enable_max_depth: true
|
||||
|
||||
trusted_proxies: 'private_ranges'
|
||||
# trust *all* "X-Forwarded-*" headers
|
||||
trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ]
|
||||
|
||||
session:
|
||||
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
8
config/packages/mercure.yaml
Normal file
8
config/packages/mercure.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
mercure:
|
||||
hubs:
|
||||
default:
|
||||
url: '%env(MERCURE_URL)%'
|
||||
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||
jwt:
|
||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||
publish: '*'
|
||||
@@ -1,11 +1,19 @@
|
||||
framework:
|
||||
messenger:
|
||||
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
||||
# failure_transport: failed
|
||||
failure_transport: failed
|
||||
|
||||
transports:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
use_notify: true
|
||||
check_delayed_interval: 60000
|
||||
retry_strategy:
|
||||
max_retries: 1
|
||||
multiplier: 1
|
||||
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
|
||||
default_bus: messenger.bus.default
|
||||
@@ -17,6 +25,10 @@ framework:
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
'App\Download\Action\Command\DownloadMediaCommand': 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
|
||||
|
||||
# when@test:
|
||||
# framework:
|
||||
|
||||
@@ -5,13 +5,26 @@ security:
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\User\Framework\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: app_logout
|
||||
# where to redirect after logout
|
||||
# target: app_any_route
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
@@ -22,8 +35,9 @@ 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: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
7
config/packages/translation.yaml
Normal file
7
config/packages/translation.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
providers:
|
||||
11
config/packages/web_profiler.yaml
Normal file
11
config/packages/web_profiler.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
@@ -6,3 +6,18 @@ controllersIndex:
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersUser:
|
||||
resource:
|
||||
path: ../src/User/Framework/Controller
|
||||
namespace: App\User\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: ['https']
|
||||
|
||||
controllersMonitor:
|
||||
resource:
|
||||
path: ../src/Monitor/Framework/Controller
|
||||
namespace: App\Monitor\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: ['https']
|
||||
|
||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
@@ -4,6 +4,10 @@
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
media.default_movies_dir: movies
|
||||
media.default_tvshows_dir: tvshows
|
||||
media.movies_path: '/var/download/%env(default:media.default_movies_dir:MOVIES_PATH)%'
|
||||
media.tvshows_path: '/var/download/%env(default:media.default_tvshows_dir:TVSHOWS_PATH)%'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -23,3 +27,7 @@ services:
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
||||
|
||||
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
||||
arguments:
|
||||
- '%env(DATABASE_URL)%'
|
||||
|
||||
@@ -12,4 +12,31 @@ services:
|
||||
- /mnt/media/downloads:/var/download
|
||||
command: php ./bin/console messenger:consume async -v --time-limit=3600 --limit=10
|
||||
deploy:
|
||||
replicas: 2
|
||||
replicas: 2
|
||||
|
||||
scheduler:
|
||||
image: registry.caldwell.digital/home/torsearch/app:${TAG}
|
||||
volumes:
|
||||
- /mnt/media/downloads:/var/download
|
||||
command: php ./bin/console messenger:consume scheduler_monitor -vv --time-limit=3600
|
||||
|
||||
mercure:
|
||||
image: dunglas/mercure
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
SERVER_NAME: ':80'
|
||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_EXTRA_DIRECTIVES: |
|
||||
cors_origins *
|
||||
anonymous
|
||||
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
|
||||
volumes:
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
|
||||
volumes:
|
||||
mercure_config:
|
||||
mercure_data:
|
||||
|
||||
@@ -25,4 +25,7 @@ return [
|
||||
'@symfony/ux-live-component' => [
|
||||
'path' => './vendor/symfony/ux-live-component/assets/dist/live_controller.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
||||
|
||||
46
migrations/Version20250428133608.php
Normal file
46
migrations/Version20250428133608.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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 Version20250428133608 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE preference (id VARCHAR(255) NOT NULL, name VARCHAR(255) DEFAULT NULL, description VARCHAR(255) DEFAULT NULL, enabled TINYINT(1) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE preference_option (id INT AUTO_INCREMENT NOT NULL, preference_id VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, value VARCHAR(255) DEFAULT NULL, enabled TINYINT(1) NOT NULL, INDEX IDX_607C52FD81022C0 (preference_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE preference_option ADD CONSTRAINT FK_607C52FD81022C0 FOREIGN KEY (preference_id) REFERENCES preference (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 preference_option DROP FOREIGN KEY FK_607C52FD81022C0
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE preference
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE preference_option
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
83
migrations/Version20250428140450.php
Normal file
83
migrations/Version20250428140450.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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 Version20250428140450 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 user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL COMMENT '(DC2Type:json)', password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE episode DROP FOREIGN KEY FK_DDAA1CDAACB7A4A
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE processed_messages
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE episode
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE series
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE messenger_messages CHANGE id id BIGINT AUTO_INCREMENT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE processed_messages (id INT AUTO_INCREMENT NOT NULL, run_id INT NOT NULL, attempt SMALLINT NOT NULL, message_type VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, description VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, dispatched_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', received_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', finished_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', wait_time BIGINT NOT NULL, handle_time BIGINT NOT NULL, memory_usage INT NOT NULL, transport VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tags VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, failure_type VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, failure_message LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, results JSON DEFAULT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE episode (id INT AUTO_INCREMENT NOT NULL, series_id_id INT DEFAULT NULL, imdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, tvdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, year VARCHAR(5) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, poster VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, season VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, episode VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, episode_code VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, download_directory VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, INDEX IDX_DDAA1CDAACB7A4A (series_id_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE series (id INT AUTO_INCREMENT NOT NULL, imdb_id VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tvdb_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, year VARCHAR(5) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, poster VARCHAR(500) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, directory VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, number_seasons INT DEFAULT NULL, number_episodes INT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE episode ADD CONSTRAINT FK_DDAA1CDAACB7A4A FOREIGN KEY (series_id_id) REFERENCES series (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE user
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_75EA56E0FB7336F0 ON messenger_messages
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_75EA56E0E3BD61CE ON messenger_messages
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_75EA56E016BA31DB ON messenger_messages
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE messenger_messages CHANGE id id INT AUTO_INCREMENT NOT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20250429020903.php
Normal file
35
migrations/Version20250429020903.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 Version20250429020903 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 ADD name 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 DROP name
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
47
migrations/Version20250429032311.php
Normal file
47
migrations/Version20250429032311.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 Version20250429032311 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 user_preference (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, preference_id VARCHAR(255) NOT NULL, preference_value VARCHAR(255) DEFAULT NULL, INDEX IDX_FA0E76BFA76ED395 (user_id), INDEX IDX_FA0E76BFD81022C0 (preference_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference ADD CONSTRAINT FK_FA0E76BFA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference ADD CONSTRAINT FK_FA0E76BFD81022C0 FOREIGN KEY (preference_id) REFERENCES preference (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 user_preference DROP FOREIGN KEY FK_FA0E76BFA76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user_preference DROP FOREIGN KEY FK_FA0E76BFD81022C0
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE user_preference
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20250430231033.php
Normal file
35
migrations/Version20250430231033.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 Version20250430231033 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 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
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE sessions
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
41
migrations/Version20250503034641.php
Normal file
41
migrations/Version20250503034641.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 Version20250503034641 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 movie_monitor (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, title VARCHAR(255) DEFAULT NULL, imdb_id VARCHAR(255) NOT NULL, tmdb_id VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, search_count INT DEFAULT NULL, last_search DATETIME DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', downloaded_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_C183DBABA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE movie_monitor ADD CONSTRAINT FK_C183DBABA76ED395 FOREIGN KEY (user_id) REFERENCES 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 movie_monitor DROP FOREIGN KEY FK_C183DBABA76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE movie_monitor
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
53
migrations/Version20250505211458.php
Normal file
53
migrations/Version20250505211458.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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 Version20250505211458 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 monitor (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, title VARCHAR(255) DEFAULT NULL, imdb_id VARCHAR(255) NOT NULL, tmdb_id VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, search_count INT DEFAULT NULL, last_search DATETIME DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', downloaded_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', monitor_type VARCHAR(255) NOT NULL, season INT DEFAULT NULL, episode INT DEFAULT NULL, INDEX IDX_E1159985A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE monitor ADD CONSTRAINT FK_E1159985A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE movie_monitor DROP FOREIGN KEY FK_C183DBABA76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE movie_monitor
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE movie_monitor (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, imdb_id VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, tmdb_id VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, status VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, search_count INT DEFAULT NULL, last_search DATETIME DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', downloaded_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_C183DBABA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = ''
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE movie_monitor ADD CONSTRAINT FK_C183DBABA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE monitor DROP FOREIGN KEY FK_E1159985A76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE monitor
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
8
phpstan.dist.neon
Normal file
8
phpstan.dist.neon
Normal file
@@ -0,0 +1,8 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- bin/
|
||||
- config/
|
||||
- public/
|
||||
- src/
|
||||
- tests/
|
||||
37
src/Controller/AlertController.php
Normal file
37
src/Controller/AlertController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final class AlertController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')] private readonly Environment $renderer,
|
||||
private readonly HubInterface $hub,
|
||||
) {}
|
||||
|
||||
#[Route('/alert', name: 'app_alert')]
|
||||
public function index(): Response
|
||||
{
|
||||
$update = new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('broadcast/Alert.html.twig', [
|
||||
'alert_id' => 1,
|
||||
'title' => 'Added to queue',
|
||||
'message' => 'This is a testy test!',
|
||||
])
|
||||
);
|
||||
|
||||
$this->hub->publish($update);
|
||||
return $this->json([
|
||||
'Success' => 'Published'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Download\Action\Input\DownloadMediaInput;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
@@ -11,6 +12,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
class DownloadController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
private MessageBusInterface $bus,
|
||||
) {}
|
||||
|
||||
@@ -18,6 +20,15 @@ class DownloadController extends AbstractController
|
||||
public function download(
|
||||
DownloadMediaInput $input,
|
||||
): Response {
|
||||
$download = $this->downloadRepository->insert(
|
||||
$input->url,
|
||||
$input->title,
|
||||
$input->filename,
|
||||
$input->imdbId,
|
||||
$input->mediaType,
|
||||
"",
|
||||
);
|
||||
$input->downloadId = $download->getId();
|
||||
try {
|
||||
$this->bus->dispatch($input->toCommand());
|
||||
} catch (\Throwable $exception) {
|
||||
|
||||
@@ -22,6 +22,7 @@ final class IndexController extends AbstractController
|
||||
'active_downloads' => $this->downloadRepository->getActivePaginated(),
|
||||
'recent_downloads' => $this->downloadRepository->latest(5),
|
||||
'popular_movies' => $this->tmdb->popularMovies(1, 6),
|
||||
'popular_tvshows' => $this->tmdb->popularTvShows(1, 6),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ use App\Search\Action\Handler\GetMediaInfoHandler;
|
||||
use App\Search\Action\Handler\SearchHandler;
|
||||
use App\Search\Action\Input\GetMediaInfoInput;
|
||||
use App\Search\Action\Input\SearchInput;
|
||||
use Carbon\Carbon;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
final class SearchController extends AbstractController
|
||||
{
|
||||
@@ -30,20 +33,25 @@ final class SearchController extends AbstractController
|
||||
|
||||
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
|
||||
public function result(
|
||||
GetMediaInfoInput $getDownloadOptionsInput,
|
||||
GetMediaInfoInput $input,
|
||||
CacheInterface $cache
|
||||
): Response {
|
||||
$result = $this->getMediaInfoHandler->handle($getDownloadOptionsInput->toCommand());
|
||||
$cacheId = sprintf("page.%s.%s", $input->mediaType, $input->tmdbId);
|
||||
|
||||
return $this->render('search/result.html.twig', [
|
||||
'results' => $result,
|
||||
'filter' => [
|
||||
'resolution' => '',
|
||||
'codec' => '',
|
||||
'provider' => '',
|
||||
'language' => '',
|
||||
'season' => 1,
|
||||
'episode' => ''
|
||||
]
|
||||
]);
|
||||
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||
return $this->render('search/result.html.twig', [
|
||||
'results' => $result,
|
||||
'filter' => [
|
||||
'resolution' => '',
|
||||
'codec' => '',
|
||||
'provider' => '',
|
||||
'language' => '',
|
||||
'season' => 1,
|
||||
'episode' => ''
|
||||
]
|
||||
]);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,34 +6,89 @@ use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use App\Torrentio\Action\Input\GetMovieOptionsInput;
|
||||
use App\Torrentio\Action\Input\GetTvShowOptionsInput;
|
||||
use Carbon\Carbon;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
final class TorrentioController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly \Twig\Environment $renderer,
|
||||
) {}
|
||||
|
||||
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
|
||||
public function movieOptions(GetMovieOptionsInput $input): Response
|
||||
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
|
||||
{
|
||||
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.movies.%s.%s",
|
||||
$input->tmdbId,
|
||||
$input->imdbId
|
||||
);
|
||||
|
||||
return $this->render('torrentio/movies.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
|
||||
return $this->render('torrentio/movies.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
|
||||
public function tvShowOptions(GetTvShowOptionsInput $input): Response
|
||||
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
|
||||
{
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||
$input->tmdbId,
|
||||
$input->imdbId,
|
||||
$input->season,
|
||||
$input->episode,
|
||||
);
|
||||
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
|
||||
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
|
||||
{
|
||||
$cacheId = sprintf(
|
||||
"page.torrentio.tvshows.%s.%s.%s.%s",
|
||||
$input->tmdbId,
|
||||
$input->imdbId,
|
||||
$input->season,
|
||||
$input->episode,
|
||||
);
|
||||
$cache->delete($cacheId);
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('broadcast/Alert.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => 'Torrentio cache Cleared.',
|
||||
])
|
||||
));
|
||||
|
||||
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
|
||||
return $this->render('torrentio/tvshows.html.twig', [
|
||||
'results' => $results,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ class DownloadMediaCommand implements CommandInterface
|
||||
public string $filename,
|
||||
public string $mediaType,
|
||||
public string $imdbId,
|
||||
public ?int $downloadId = null,
|
||||
) {}
|
||||
}
|
||||
@@ -21,14 +21,19 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$download = $this->downloadRepository->insert(
|
||||
$command->url,
|
||||
$command->title,
|
||||
$command->filename,
|
||||
$command->imdbId,
|
||||
$command->mediaType,
|
||||
""
|
||||
);
|
||||
if (null === $command->downloadId) {
|
||||
$download = $this->downloadRepository->insert(
|
||||
$command->url,
|
||||
$command->title,
|
||||
$command->filename,
|
||||
$command->imdbId,
|
||||
$command->mediaType,
|
||||
""
|
||||
);
|
||||
} else {
|
||||
$download = $this->downloadRepository->find($command->downloadId);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$this->downloadRepository->updateStatus($download->getId(), 'In Progress');
|
||||
|
||||
@@ -25,6 +25,8 @@ class DownloadMediaInput implements InputInterface
|
||||
|
||||
#[SourceRequest('imdbId')]
|
||||
public string $imdbId,
|
||||
|
||||
public ?int $downloadId = null,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
@@ -35,6 +37,7 @@ class DownloadMediaInput implements InputInterface
|
||||
$this->filename,
|
||||
$this->mediaType,
|
||||
$this->imdbId,
|
||||
$this->downloadId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DownloadRepository::class)]
|
||||
#[Broadcast]
|
||||
#[Broadcast(template: 'broadcast/Download.stream.html.twig')]
|
||||
class Download
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Framework\MessageHandler;
|
||||
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\Action\Handler\DownloadMediaHandler;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(handles: DownloadMediaCommand::class)]
|
||||
class DownloadMediaMessageHandler
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadMediaHandler $downloadMediaHandler,
|
||||
) {}
|
||||
|
||||
public function __invoke(DownloadMediaCommand $command)
|
||||
{
|
||||
$this->downloadMediaHandler->handle($command);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
$firstResult = ($pageNumber - 1) * $perPage;
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->orderBy('d.id', 'DESC')
|
||||
->orderBy('d.id', 'ASC')
|
||||
->setParameter('statuses', ['New', 'In Progress'])
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($perPage)
|
||||
@@ -66,6 +66,7 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
->setImdbId($imdbId)
|
||||
->setMediaType($mediaType)
|
||||
->setBatchId($batchId)
|
||||
->setProgress(0)
|
||||
->setStatus($status);
|
||||
|
||||
$this->getEntityManager()->persist($download);
|
||||
|
||||
18
src/Monitor/Action/Command/AddMonitorCommand.php
Normal file
18
src/Monitor/Action/Command/AddMonitorCommand.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class AddMonitorCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userEmail,
|
||||
public string $title,
|
||||
public string $imdbId,
|
||||
public string $tmdbId,
|
||||
public string $monitorType,
|
||||
public ?int $season,
|
||||
public ?int $episode,
|
||||
) {}
|
||||
}
|
||||
12
src/Monitor/Action/Command/MonitorMovieCommand.php
Normal file
12
src/Monitor/Action/Command/MonitorMovieCommand.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class MonitorMovieCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $movieMonitorId,
|
||||
) {}
|
||||
}
|
||||
12
src/Monitor/Action/Command/MonitorTvEpisodeCommand.php
Normal file
12
src/Monitor/Action/Command/MonitorTvEpisodeCommand.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class MonitorTvEpisodeCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $movieMonitorId,
|
||||
) {}
|
||||
}
|
||||
12
src/Monitor/Action/Command/MonitorTvSeasonCommand.php
Normal file
12
src/Monitor/Action/Command/MonitorTvSeasonCommand.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class MonitorTvSeasonCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $monitorId,
|
||||
) {}
|
||||
}
|
||||
12
src/Monitor/Action/Command/MonitorTvShowCommand.php
Normal file
12
src/Monitor/Action/Command/MonitorTvShowCommand.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
class MonitorTvShowCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public int $monitorId,
|
||||
) {}
|
||||
}
|
||||
48
src/Monitor/Action/Handler/AddMonitorHandler.php
Normal file
48
src/Monitor/Action/Handler/AddMonitorHandler.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
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\User\Framework\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements HandlerInterface<AddMonitorCommand> */
|
||||
readonly class AddMonitorHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MonitorRepository $movieMonitorRepository,
|
||||
private UserRepository $userRepository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$user = $this->userRepository->findOneBy(['email' => $command->userEmail]);
|
||||
$monitor = (new Monitor())
|
||||
->setUser($user)
|
||||
->setTmdbId($command->tmdbId)
|
||||
->setImdbId($command->imdbId)
|
||||
->setTitle($command->title)
|
||||
->setMonitorType($command->monitorType)
|
||||
->setSeason($command->season)
|
||||
->setEpisode($command->episode)
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->movieMonitorRepository->getEntityManager()->persist($monitor);
|
||||
$this->movieMonitorRepository->getEntityManager()->flush();
|
||||
|
||||
return new AddMonitorResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
72
src/Monitor/Action/Handler/MonitorMovieHandler.php
Normal file
72
src/Monitor/Action/Handler/MonitorMovieHandler.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use App\Monitor\Action\Command\DownloadMediaCommand;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\Service\MonitorOptionEvaluator;
|
||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||
use App\Torrentio\Action\Handler\GetMovieOptionsHandler;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||
readonly class MonitorMovieHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MonitorRepository $movieMonitorRepository,
|
||||
private GetMovieOptionsHandler $getMovieOptionsHandler,
|
||||
private MonitorOptionEvaluator $monitorOptionEvaluator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MessageBusInterface $bus,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorMovieHandler] Executing MonitorMovieHandler');
|
||||
$monitor = $this->movieMonitorRepository->find($command->movieMonitorId);
|
||||
$monitor->setStatus('In Progress');
|
||||
|
||||
$this->logger->info('> [MonitorMovieHandler] Searching for "' . $monitor->getTitle() . '" download options');
|
||||
$results = $this->getMovieOptionsHandler->handle(
|
||||
new GetMovieOptionsCommand($monitor->getTmdbId(), $monitor->getImdbId())
|
||||
);
|
||||
$this->logger->info('> [MonitorMovieHandler] Found ' . count($results->results) . ' download options');
|
||||
|
||||
$result = $this->monitorOptionEvaluator->evaluateOptions($monitor, $results->results);
|
||||
|
||||
if (null !== $result) {
|
||||
$this->logger->info('> [MonitorMovieHandler] 1 result found: dispatching DownloadMediaCommand for "' . $result->title . '"');
|
||||
$this->bus->dispatch(new DownloadMediaCommand(
|
||||
$result->url,
|
||||
$monitor->getTitle(),
|
||||
$result->filename,
|
||||
'movies',
|
||||
$monitor->getImdbId(),
|
||||
));
|
||||
$monitor->setStatus('Complete');
|
||||
$monitor->setDownloadedAt(new DateTimeIMmutable());
|
||||
} else {
|
||||
$monitor->setStatus('Active');
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new MonitorMovieResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
80
src/Monitor/Action/Handler/MonitorTvEpisodeHandler.php
Normal file
80
src/Monitor/Action/Handler/MonitorTvEpisodeHandler.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\Service\MonitorOptionEvaluator;
|
||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||
readonly class MonitorTvEpisodeHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetTvShowOptionsHandler $getTvShowOptionsHandler,
|
||||
private MonitorOptionEvaluator $monitorOptionEvaluator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MessageBusInterface $bus,
|
||||
private LoggerInterface $logger,
|
||||
private MonitorRepository $monitorRepository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] Executing MonitorTvEpisodeHandler');
|
||||
$monitor = $this->monitorRepository->find($command->movieMonitorId);
|
||||
$monitor->setStatus('In Progress');
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] Searching for "' . $monitor->getTitle() . '" season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode() . ' download options');
|
||||
$results = $this->getTvShowOptionsHandler->handle(
|
||||
new GetTvShowOptionsCommand(
|
||||
$monitor->getTmdbId(),
|
||||
$monitor->getImdbId(),
|
||||
$monitor->getSeason(),
|
||||
$monitor->getEpisode()
|
||||
)
|
||||
);
|
||||
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] Found ' . count($results->results) . ' download options');
|
||||
|
||||
$result = $this->monitorOptionEvaluator->evaluateOptions($monitor, $results->results);
|
||||
|
||||
if (null !== $result) {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] 1 matching result found: dispatching DownloadMediaCommand for "' . $result->title . '"');
|
||||
$this->bus->dispatch(new DownloadMediaCommand(
|
||||
$result->url,
|
||||
$monitor->getTitle(),
|
||||
$result->filename,
|
||||
'tvshows',
|
||||
$monitor->getImdbId(),
|
||||
));
|
||||
$monitor->setStatus('Complete');
|
||||
$monitor->setDownloadedAt(new DateTimeImmutable());
|
||||
} else {
|
||||
$this->logger->info('> [MonitorTvEpisodeHandler] 0 matching results found, monitor will run at next interval');
|
||||
$monitor->setStatus('Active');
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new MonitorMovieResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
103
src/Monitor/Action/Handler/MonitorTvSeasonHandler.php
Normal file
103
src/Monitor/Action/Handler/MonitorTvSeasonHandler.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||
use App\Monitor\Action\Result\MonitorMovieResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Nihilarr\PTN;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||
readonly class MonitorTvSeasonHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MonitorRepository $monitorRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MediaFiles $mediaFiles,
|
||||
private MessageBusInterface $bus,
|
||||
private LoggerInterface $logger,
|
||||
private Tmdb $tmdb,
|
||||
private MonitorTvEpisodeHandler $monitorTvEpisodeHandler,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorTvSeasonHandler] Executing MonitorTvSeasonHandler');
|
||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||
|
||||
// Check current episodes
|
||||
$downloadedEpisodes = $this->mediaFiles
|
||||
->getEpisodes($monitor->getTitle())
|
||||
->map(fn($episode) => (object) (new PTN())->parse($episode))
|
||||
->rekey(fn($episode) => $episode->episode);
|
||||
$this->logger->info('> [MonitorTvSeasonHandler] Found ' . count($downloadedEpisodes) . ' downloaded episodes for title: ' . $monitor->getTitle());
|
||||
|
||||
// Compare against list from TMDB
|
||||
$episodesInSeason = Map::from(
|
||||
$this->tmdb->tvDetails($monitor->getTmdbId())
|
||||
->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());
|
||||
|
||||
// Dispatch Episode commands for each missing Episode
|
||||
foreach ($episodesInSeason as $episode) {
|
||||
$monitorCheck = $this->monitorRepository->findOneBy([
|
||||
'imdbId' => $monitor->getImdbId(),
|
||||
'title' => $monitor->getTitle(),
|
||||
'monitorType' => 'tvepisode',
|
||||
'season' => $monitor->getSeason(),
|
||||
'episode' => $episode['episode_number'],
|
||||
'status' => ['New', 'Active', 'In Progress']
|
||||
]);
|
||||
|
||||
$this->logger->info('> [MonitorTvSeasonHandler] Monitor exists for season ' . $monitor->getSeason() . ' episode ' . $episode['episode_number'] . ' for title: ' . $monitor->getTitle() . ' ? ' . (null !== $monitorCheck ? 'YES' : 'NO'));
|
||||
|
||||
if (!array_key_exists($episode['episode_number'], $downloadedEpisodes->toArray())
|
||||
&& null === $monitorCheck
|
||||
) {
|
||||
$episodeMonitor = (new Monitor())
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvepisode')
|
||||
->setSeason($monitor->getSeason())
|
||||
->setEpisode($episode['episode_number'])
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($episodeMonitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$command = new MonitorTvEpisodeCommand($episodeMonitor->getId());
|
||||
$this->monitorTvEpisodeHandler->handle($command);
|
||||
$this->logger->info('> [MonitorTvSeasonHandler] Dispatching MonitorTvEpisodeCommand for season ' . $episodeMonitor->getSeason() . ' episode ' . $episodeMonitor->getEpisode() . ' for title: ' . $monitor->getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||
$monitor->setStatus('Complete');
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new MonitorMovieResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/Monitor/Action/Handler/MonitorTvShowHandler.php
Normal file
93
src/Monitor/Action/Handler/MonitorTvShowHandler.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Handler;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||
use App\Monitor\Action\Result\MonitorTvEpisodeResult;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\Monitor\Service\MediaFiles;
|
||||
use App\Tmdb\Tmdb;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Nihilarr\PTN;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
/** @implements HandlerInterface<MonitorMovieCommand> */
|
||||
readonly class MonitorTvShowHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private MonitorRepository $monitorRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private MediaFiles $mediaFiles,
|
||||
private MessageBusInterface $bus,
|
||||
private LoggerInterface $logger,
|
||||
private Tmdb $tmdb,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorTvShowHandler] Executing MonitorTvShowHandler');
|
||||
$monitor = $this->monitorRepository->find($command->monitorId);
|
||||
|
||||
// Check current episodes
|
||||
$downloadedEpisodes = $this->mediaFiles
|
||||
->getEpisodes($monitor->getTitle())
|
||||
->map(fn($episode) => (object) (new PTN())->parse($episode));
|
||||
$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->logger->info('> [MonitorTvShowHandler] Found ' . count($episodesInShow) . ' episodes in season ' . $monitor->getSeason() . ' for title: ' . $monitor->getTitle());
|
||||
|
||||
// Dispatch Episode commands for each missing Episode
|
||||
foreach ($episodesInShow as $episode) {
|
||||
$episodeAlreadyDownloaded = $downloadedEpisodes->find(
|
||||
fn($ep) => $ep->episode === $episode['episode_number'] && $ep->season === $episode['season_number']
|
||||
);
|
||||
$episodeAlreadyDownloaded = !is_null($episodeAlreadyDownloaded);
|
||||
|
||||
if (false === $episodeAlreadyDownloaded) {
|
||||
$monitor = (new Monitor())
|
||||
->setUser($monitor->getUser())
|
||||
->setTmdbId($monitor->getTmdbId())
|
||||
->setImdbId($monitor->getImdbId())
|
||||
->setTitle($monitor->getTitle())
|
||||
->setMonitorType('tvshow')
|
||||
->setSeason($episode['season_number'])
|
||||
->setEpisode($episode['episode_number'])
|
||||
->setCreatedAt(new DateTimeImmutable())
|
||||
->setSearchCount(0)
|
||||
->setStatus('New');
|
||||
|
||||
$this->monitorRepository->getEntityManager()->persist($monitor);
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$command = new MonitorTvEpisodeCommand($monitor->getId());
|
||||
$this->bus->dispatch($command);
|
||||
$this->logger->info('> [MonitorTvShowHandler] Dispatching MonitorTvEpisodeCommand for season ' . $monitor->getSeason() . ' episode ' . $monitor->getEpisode() . ' for title: ' . $monitor->getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->setSearchCount($monitor->getSearchCount() + 1);
|
||||
$monitor->setStatus('Complete');
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new MonitorTvEpisodeResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/Monitor/Action/Input/AddMonitorInput.php
Normal file
48
src/Monitor/Action/Input/AddMonitorInput.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Input;
|
||||
|
||||
use App\Monitor\Action\Command\AddMonitorCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
class AddMonitorInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceSecurity]
|
||||
public string $userEmail,
|
||||
|
||||
#[SourceRequest('tmdbId')]
|
||||
public string $tmdbId,
|
||||
|
||||
#[SourceRequest('imdbId')]
|
||||
public string $imdbId,
|
||||
|
||||
#[SourceRequest('title')]
|
||||
public string $title,
|
||||
|
||||
#[SourceRequest('monitorType')]
|
||||
public string $monitorType,
|
||||
|
||||
#[SourceRequest('season', nullify: true)]
|
||||
public ?int $season,
|
||||
|
||||
#[SourceRequest('episode', nullify: true)]
|
||||
public ?int $episode,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new AddMonitorCommand(
|
||||
$this->userEmail,
|
||||
$this->title,
|
||||
$this->imdbId,
|
||||
$this->tmdbId,
|
||||
$this->monitorType,
|
||||
$this->season,
|
||||
$this->episode,
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/Monitor/Action/Input/MonitorMovieInput.php
Normal file
24
src/Monitor/Action/Input/MonitorMovieInput.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Input;
|
||||
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRoute;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
class MonitorMovieInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceRoute('tmdbId')]
|
||||
public string $tmdbId,
|
||||
|
||||
#[SourceRoute('imdbId')]
|
||||
public string $imdbId,
|
||||
) {}
|
||||
|
||||
public function toCommand(): CommandInterface
|
||||
{
|
||||
return new MonitorMovieCommand($this->tmdbId, $this->imdbId);
|
||||
}
|
||||
}
|
||||
13
src/Monitor/Action/Result/AddMonitorResult.php
Normal file
13
src/Monitor/Action/Result/AddMonitorResult.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Result;
|
||||
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class AddMonitorResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public array $result,
|
||||
) {}
|
||||
}
|
||||
13
src/Monitor/Action/Result/MonitorMovieResult.php
Normal file
13
src/Monitor/Action/Result/MonitorMovieResult.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Result;
|
||||
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class MonitorMovieResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public array $result,
|
||||
) {}
|
||||
}
|
||||
13
src/Monitor/Action/Result/MonitorTvEpisodeResult.php
Normal file
13
src/Monitor/Action/Result/MonitorTvEpisodeResult.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Action\Result;
|
||||
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
class MonitorTvEpisodeResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public array $result,
|
||||
) {}
|
||||
}
|
||||
44
src/Monitor/Framework/Controller/ApiController.php
Normal file
44
src/Monitor/Framework/Controller/ApiController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Controller;
|
||||
|
||||
use App\Monitor\Action\Handler\AddMonitorHandler;
|
||||
use App\Monitor\Action\Input\AddMonitorInput;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
class ApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'twig')]
|
||||
private readonly Environment $renderer,
|
||||
private readonly HubInterface $hub,
|
||||
) {}
|
||||
|
||||
#[Route('/api/monitor', name: 'api_monitor', methods: ['POST'])]
|
||||
public function addMonitor(
|
||||
AddMonitorInput $input,
|
||||
AddMonitorHandler $handler,
|
||||
HubInterface $hub,
|
||||
) {
|
||||
$response = $handler->handle($input->toCommand());
|
||||
|
||||
$hub->publish(new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('broadcast/Alert.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => "New monitor added for {$input->title}",
|
||||
])
|
||||
));
|
||||
|
||||
return $this->json([
|
||||
'status' => 200,
|
||||
'message' => $response
|
||||
]);
|
||||
}
|
||||
}
|
||||
0
src/Monitor/Framework/Entity/.gitignore
vendored
Normal file
0
src/Monitor/Framework/Entity/.gitignore
vendored
Normal file
205
src/Monitor/Framework/Entity/Monitor.php
Normal file
205
src/Monitor/Framework/Entity/Monitor.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Entity;
|
||||
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MonitorRepository::class)]
|
||||
class Monitor
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[Ignore]
|
||||
#[ORM\ManyToOne(inversedBy: 'yes')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $imdbId = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $tmdbId = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $monitorType = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $season = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $episode = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $status = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $searchCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||
private ?\DateTimeInterface $lastSearch = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $downloadedAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImdbId(): ?string
|
||||
{
|
||||
return $this->imdbId;
|
||||
}
|
||||
|
||||
public function setImdbId(string $imdbId): static
|
||||
{
|
||||
$this->imdbId = $imdbId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTmdbId(): ?string
|
||||
{
|
||||
return $this->tmdbId;
|
||||
}
|
||||
|
||||
public function setTmdbId(string $tmdbId): static
|
||||
{
|
||||
$this->tmdbId = $tmdbId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSearchCount(): ?int
|
||||
{
|
||||
return $this->searchCount;
|
||||
}
|
||||
|
||||
public function setSearchCount(?int $searchCount): static
|
||||
{
|
||||
$this->searchCount = $searchCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastSearch(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->lastSearch;
|
||||
}
|
||||
|
||||
public function setLastSearch(?\DateTimeInterface $lastSearch): static
|
||||
{
|
||||
$this->lastSearch = $lastSearch;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDownloadedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->downloadedAt;
|
||||
}
|
||||
|
||||
public function setDownloadedAt(?\DateTimeImmutable $downloadedAt): static
|
||||
{
|
||||
$this->downloadedAt = $downloadedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMonitorType(): ?string
|
||||
{
|
||||
return $this->monitorType;
|
||||
}
|
||||
|
||||
public function setMonitorType(string $monitorType): static
|
||||
{
|
||||
$this->monitorType = $monitorType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSeason(): ?int
|
||||
{
|
||||
return $this->season;
|
||||
}
|
||||
|
||||
public function setSeason(?int $season): static
|
||||
{
|
||||
$this->season = $season;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEpisode(): ?int
|
||||
{
|
||||
return $this->episode;
|
||||
}
|
||||
|
||||
public function setEpisode(?int $episode): static
|
||||
{
|
||||
$this->episode = $episode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
0
src/Monitor/Framework/Repository/.gitignore
vendored
Normal file
0
src/Monitor/Framework/Repository/.gitignore
vendored
Normal file
43
src/Monitor/Framework/Repository/MonitorRepository.php
Normal file
43
src/Monitor/Framework/Repository/MonitorRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Repository;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Monitor>
|
||||
*/
|
||||
class MonitorRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Monitor::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return MovieMonitor[] Returns an array of MovieMonitor objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('m.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?MovieMonitor
|
||||
// {
|
||||
// return $this->createQueryBuilder('m')
|
||||
// ->andWhere('m.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
44
src/Monitor/Framework/Scheduler/MonitorDispatcher.php
Normal file
44
src/Monitor/Framework/Scheduler/MonitorDispatcher.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Scheduler;
|
||||
|
||||
use App\Monitor\Action\Command\MonitorMovieCommand;
|
||||
use App\Monitor\Action\Command\MonitorTvEpisodeCommand;
|
||||
use App\Monitor\Action\Command\MonitorTvSeasonCommand;
|
||||
use App\Monitor\Action\Command\MonitorTvShowCommand;
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Scheduler\Attribute\AsCronTask;
|
||||
|
||||
#[AsCronTask('*/10 * * * *', schedule: 'monitor')]
|
||||
class MonitorDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly MonitorRepository $monitorRepository,
|
||||
private readonly MessageBusInterface $bus,
|
||||
) {}
|
||||
|
||||
public function __invoke() {
|
||||
$this->logger->info('[MonitorDispatcher] Executing MonitorDispatcher');
|
||||
|
||||
$monitorHandlers = [
|
||||
'movie' => MonitorMovieCommand::class,
|
||||
'tvepisode' => MonitorTvEpisodeCommand::class,
|
||||
'tvseason' => MonitorTvSeasonCommand::class,
|
||||
'tvshow' => MonitorTvShowCommand::class,
|
||||
];
|
||||
|
||||
$monitors = $this->monitorRepository->findBy(['status' => ['New', 'Active']]);
|
||||
|
||||
foreach ($monitors as $monitor) {
|
||||
$monitor->setStatus('In Progress');
|
||||
$this->monitorRepository->getEntityManager()->flush();
|
||||
|
||||
$command = $monitorHandlers[$monitor->getMonitorType()];
|
||||
$this->logger->info('[MonitorDispatcher] Dispatching ' . $command . ' for ' . $monitor->getTitle());
|
||||
$this->bus->dispatch(new $command($monitor->getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Monitor/Service/MediaFiles.php
Normal file
86
src/Monitor/Service/MediaFiles.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Service;
|
||||
|
||||
use Aimeos\Map;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class MediaFiles
|
||||
{
|
||||
private Finder $finder;
|
||||
|
||||
private string $moviesPath;
|
||||
|
||||
private string $tvShowsPath;
|
||||
|
||||
private Filesystem $filesystem;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(param: 'media.movies_path')]
|
||||
string $moviesPath,
|
||||
|
||||
#[Autowire(param: 'media.tvshows_path')]
|
||||
string $tvShowsPath,
|
||||
|
||||
Filesystem $filesystem,
|
||||
) {
|
||||
$this->finder = new Finder();
|
||||
$this->moviesPath = $moviesPath;
|
||||
$this->tvShowsPath = $tvShowsPath;
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
public function getMoviesPath(): string
|
||||
{
|
||||
return $this->moviesPath;
|
||||
}
|
||||
|
||||
public function getTvShowsPath(): string
|
||||
{
|
||||
return $this->tvShowsPath;
|
||||
}
|
||||
|
||||
public function getMovieDirs(): Map
|
||||
{
|
||||
$results = [];
|
||||
foreach ($this->finder->in($this->moviesPath)->directories() as $file) {
|
||||
$results[] = $file;
|
||||
}
|
||||
|
||||
return Map::from($results);
|
||||
}
|
||||
|
||||
public function getTvShowDirs(): Map
|
||||
{
|
||||
$results = [];
|
||||
foreach ($this->finder->in($this->tvShowsPath)->directories() as $file) {
|
||||
$results[] = $file;
|
||||
}
|
||||
|
||||
return Map::from($results);
|
||||
}
|
||||
|
||||
public function getEpisodes(string $path, bool $onlyFilenames = true): Map
|
||||
{
|
||||
if (!str_starts_with($path, $this->tvShowsPath)) {
|
||||
$path = $this->tvShowsPath . DIRECTORY_SEPARATOR . $path;
|
||||
}
|
||||
|
||||
if (false === $this->filesystem->exists($path)) {
|
||||
$this->filesystem->mkdir($path);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($this->finder->in($path)->files() as $file) {
|
||||
if ($onlyFilenames) {
|
||||
$results[] = $file->getRelativePathname();
|
||||
} else {
|
||||
$results[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
return Map::from($results);
|
||||
}
|
||||
}
|
||||
95
src/Monitor/Service/MonitorOptionEvaluator.php
Normal file
95
src/Monitor/Service/MonitorOptionEvaluator.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Service;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Torrentio\Result\TorrentioResult;
|
||||
|
||||
class MonitorOptionEvaluator
|
||||
{
|
||||
/**
|
||||
* @param Monitor $monitor
|
||||
* @param TorrentioResult[] $results
|
||||
* @return TorrentioResult|null
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function evaluateOptions(Monitor $monitor, array $results): ?TorrentioResult
|
||||
{
|
||||
$sizeLow = 000;
|
||||
$sizeHigh = 4096;
|
||||
|
||||
$bestMatches = [];
|
||||
$matches = [];
|
||||
|
||||
$userPreferences = $monitor->getUser()->getUserPreferenceValues();
|
||||
|
||||
foreach ($results as $result) {
|
||||
if (!in_array($userPreferences['language'], $result->languages)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($result->resolution === $userPreferences['resolution']
|
||||
&& $result->codec === $userPreferences['codec']
|
||||
) {
|
||||
$bestMatches[] = $result;
|
||||
}
|
||||
|
||||
if ($userPreferences['resolution'] === '2160p'
|
||||
&& $userPreferences['codec'] === $result->codec
|
||||
&& $result->resolution === '1080p'
|
||||
) {
|
||||
$matches[] = $result;
|
||||
}
|
||||
|
||||
if ($userPreferences['codec'] === 'h264'
|
||||
&& $userPreferences['resolution'] === $result->resolution
|
||||
&& $result->codec === 'h265'
|
||||
) {
|
||||
$matches[] = $result;
|
||||
}
|
||||
|
||||
if (($userPreferences['codec'] === null )
|
||||
&& ($userPreferences['resolution'] === null )) {
|
||||
$matches[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$sizeMatches = [];
|
||||
|
||||
foreach ($bestMatches as $result) {
|
||||
if (str_contains($result->size, 'GB')) {
|
||||
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
||||
} else {
|
||||
$size = (int) trim(str_replace('MB', '', $result->size));
|
||||
}
|
||||
|
||||
if ($size > $sizeLow && $size < $sizeHigh) {
|
||||
$sizeMatches[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sizeMatches)) {
|
||||
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||
}
|
||||
|
||||
foreach ($matches as $result) {
|
||||
$size = 0;
|
||||
if (str_contains($result->size, 'GB')) {
|
||||
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
||||
} else {
|
||||
$size = (int) trim(str_replace('MB', '', $result->size));
|
||||
}
|
||||
|
||||
if ($size > $sizeLow && $size < $sizeHigh) {
|
||||
$sizeMatches[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sizeMatches)) {
|
||||
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
29
src/Schedule.php
Normal file
29
src/Schedule.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Download\Framework\Scheduler\MonitorDispatcher;
|
||||
use Symfony\Component\Scheduler\Attribute\AsSchedule;
|
||||
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
|
||||
use Symfony\Component\Scheduler\ScheduleProviderInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsSchedule]
|
||||
class Schedule implements ScheduleProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSchedule(): SymfonySchedule
|
||||
{
|
||||
return (new SymfonySchedule())
|
||||
->stateful($this->cache) // ensure missed tasks are executed
|
||||
->processOnlyLastMissedRun(true) // ensure only last missed task is run
|
||||
|
||||
// add your own tasks here
|
||||
// see https://symfony.com/doc/current/scheduler.html#attaching-recurring-messages-to-a-schedule
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Tmdb;
|
||||
|
||||
use App\Enum\MediaType;
|
||||
use App\Tmdb\TmdbResult;
|
||||
use App\ValueObject\ResultFactory;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -18,12 +17,8 @@ use Tmdb\Event\Listener\Request\ContentTypeJsonRequestListener;
|
||||
use Tmdb\Event\Listener\Request\UserAgentRequestListener;
|
||||
use Tmdb\Event\Listener\RequestListener;
|
||||
use Tmdb\Event\RequestEvent;
|
||||
use Tmdb\Model\AbstractModel;
|
||||
use Tmdb\Model\Image;
|
||||
use Tmdb\Model\Movie;
|
||||
use Tmdb\Model\Movie\QueryParameter\AppendToResponse;
|
||||
use Tmdb\Model\Search\SearchQuery\KeywordSearchQuery;
|
||||
use Tmdb\Model\Search\SearchQuery\MovieSearchQuery;
|
||||
use Tmdb\Model\Tv;
|
||||
use Tmdb\Repository\MovieRepository;
|
||||
use Tmdb\Repository\SearchRepository;
|
||||
@@ -111,6 +106,23 @@ class Tmdb
|
||||
return $movies;
|
||||
}
|
||||
|
||||
public function popularTvShows(int $page = 1, ?int $limit = null)
|
||||
{
|
||||
$movies = $this->tvRepository->getPopular(['page' => $page]);
|
||||
|
||||
$movies = $movies->map(function ($movie) use ($movies) {
|
||||
return $this->parseResult($movies[$movie], "movie");
|
||||
});
|
||||
|
||||
$movies = array_values($movies->toArray());
|
||||
|
||||
if (null !== $limit) {
|
||||
$movies = array_slice($movies, 0, $limit);
|
||||
}
|
||||
|
||||
return $movies;
|
||||
}
|
||||
|
||||
public function search(string $term, int $page = 1)
|
||||
{
|
||||
$searchRepository = new SearchRepository($this->client);
|
||||
@@ -193,55 +205,60 @@ class Tmdb
|
||||
throw new \Exception("A media type must be set when parsing from an array.");
|
||||
}
|
||||
|
||||
function parseTvShow(array $data, string $posterBasePath): TmdbResult {
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: $posterBasePath . $data['poster_path'],
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['first_air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: $data['episodes'],
|
||||
);
|
||||
}
|
||||
|
||||
function parseEpisode(array $data, string $posterBasePath): TmdbResult {
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: $posterBasePath . $data['still_path'],
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: null,
|
||||
);
|
||||
}
|
||||
|
||||
function parseMovie(array $data, string $posterBasePath): TmdbResult {
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['title'],
|
||||
poster: $posterBasePath . $data['poster_path'],
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||
mediaType: "movies",
|
||||
);
|
||||
}
|
||||
|
||||
if ($mediaType === 'movie') {
|
||||
$result = parseMovie($data, self::POSTER_IMG_PATH);
|
||||
$result = $this->parseMovie($data, self::POSTER_IMG_PATH);
|
||||
} elseif ($mediaType === 'tvshow') {
|
||||
$result = parseTvShow($data, self::POSTER_IMG_PATH);
|
||||
$result = $this->parseTvShow($data, self::POSTER_IMG_PATH);
|
||||
} elseif ($mediaType === 'episode') {
|
||||
$result = parseEpisode($data, self::POSTER_IMG_PATH);
|
||||
$result = $this->parseEpisode($data, self::POSTER_IMG_PATH);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseTvShow(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['first_air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: $data['episodes'],
|
||||
);
|
||||
}
|
||||
|
||||
private function parseEpisode(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['name'],
|
||||
poster: (null !== $data['still_path']) ? $posterBasePath . $data['still_path'] : null,
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['air_date']))->format('Y'),
|
||||
mediaType: "tvshows",
|
||||
episodes: null,
|
||||
episodeAirDate: (new \DateTime($data['air_date']))->format('m/d/Y'),
|
||||
);
|
||||
}
|
||||
|
||||
private function parseMovie(array $data, string $posterBasePath): TmdbResult
|
||||
{
|
||||
return new TmdbResult(
|
||||
imdbId: $data['external_ids']['imdb_id'],
|
||||
tmdbId: $data['id'],
|
||||
title: $data['title'],
|
||||
poster: (null !== $data['poster_path']) ? $posterBasePath . $data['poster_path'] : null,
|
||||
description: $data['overview'],
|
||||
year: (new \DateTime($data['release_date']))->format('Y'),
|
||||
mediaType: "movies",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function parseFromObject($result): TmdbResult
|
||||
{
|
||||
$mediaType = $result instanceof Movie ? MediaType::Movie->value : MediaType::TvShow->value;
|
||||
|
||||
@@ -13,5 +13,6 @@ class TmdbResult
|
||||
public ?string $year = "",
|
||||
public ?string $mediaType = "",
|
||||
public ?array $episodes = null,
|
||||
public ?string $episodeAirDate = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Torrentio\Client\Rule\DownloadOptionFilter\Resolution;
|
||||
use App\Torrentio\Client\Rule\RuleEngine;
|
||||
use App\Torrentio\MediaResult;
|
||||
use App\Torrentio\Result\ResultFactory;
|
||||
use Carbon\Carbon;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
@@ -32,7 +33,7 @@ class Torrentio
|
||||
$cacheKey = "torrentio.{$imdbCode}";
|
||||
|
||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbCode) {
|
||||
$item->expiresAt(new \DateTimeImmutable("today 11:59 pm"));
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$response = file_get_contents(str_replace('{imdbCode}', $imdbCode, $this->searchUrl));
|
||||
return json_decode(
|
||||
$response,
|
||||
@@ -67,7 +68,7 @@ class Torrentio
|
||||
{
|
||||
$cacheKey = "torrentio.$imdbId.$season.$episode";
|
||||
$results = $this->cache->get($cacheKey, function (ItemInterface $item) use ($imdbId, $season, $episode) {
|
||||
$item->expiresAt(new \DateTimeImmutable("today 11:59 pm"));
|
||||
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
|
||||
$response = file_get_contents(str_replace('{imdbCode}', "$imdbId:$season:$episode", $this->searchUrl));
|
||||
return json_decode(
|
||||
$response,
|
||||
|
||||
@@ -3,10 +3,19 @@
|
||||
namespace App\Torrentio\Result;
|
||||
|
||||
use App\Util\CountryCodes;
|
||||
use App\Util\CountryLanguages;
|
||||
use Nihilarr\PTN;
|
||||
|
||||
class ResultFactory
|
||||
{
|
||||
public static $codecMap = [
|
||||
'h264' => 'h264',
|
||||
'h265' => 'h265',
|
||||
'x264' => 'h264',
|
||||
'x265' => 'h265',
|
||||
'-' => '-'
|
||||
];
|
||||
|
||||
public static function map(
|
||||
string $url,
|
||||
string $title,
|
||||
@@ -24,7 +33,7 @@ class ResultFactory
|
||||
$ptn->season ?? "-",
|
||||
$bingeGroup,
|
||||
$ptn->resolution ?? "-",
|
||||
$ptn->codec ?? "-",
|
||||
self::setCodec($ptn->codec ?? "-"),
|
||||
$ptn,
|
||||
substr(base64_encode($url), strlen($url) - 10),
|
||||
$ptn->episode ?? "-",
|
||||
@@ -89,16 +98,22 @@ class ResultFactory
|
||||
});
|
||||
|
||||
$languages = array_map(function ($flag) {
|
||||
return CountryCodes::convertFromAbbr(strtoupper(substr($flag['short_name'], strlen('flag-'))));
|
||||
return CountryLanguages::fromCountryCode(strtoupper(substr($flag['short_name'], strlen('flag-'))));
|
||||
},
|
||||
$flags);
|
||||
|
||||
if (count($languages) > 0) {
|
||||
return array_values($languages);
|
||||
} else {
|
||||
return ["English"];
|
||||
return ["English (US)"];
|
||||
}
|
||||
}
|
||||
|
||||
public static function setCodec(string $codec): string
|
||||
{
|
||||
return self::$codecMap[strtolower($codec)] ?? $codec;
|
||||
}
|
||||
|
||||
private static function setEpisode(string $title)
|
||||
{
|
||||
$value = [];
|
||||
|
||||
12
src/Twig/Components/Alert.php
Normal file
12
src/Twig/Components/Alert.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
|
||||
#[AsLiveComponent]
|
||||
final class Alert
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
}
|
||||
31
src/Twig/Components/Filter.php
Normal file
31
src/Twig/Components/Filter.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
|
||||
#[AsLiveComponent]
|
||||
final class Filter
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
|
||||
public array $preferences = [];
|
||||
|
||||
public array $userPreferences = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly PreferencesRepository $preferencesRepository,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
$this->preferences = Map::from($this->preferencesRepository->findEnabled())
|
||||
->rekey(fn($element) => $element->getId())
|
||||
->map(fn($element) => $element->getPreferenceOptions()->toArray())
|
||||
->toArray();
|
||||
$this->userPreferences = Map::from($this->security->getUser()->getUserPreferenceValues())
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
16
src/User/Action/Command/SaveUserMediaPreferencesCommand.php
Normal file
16
src/User/Action/Command/SaveUserMediaPreferencesCommand.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Command;
|
||||
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
|
||||
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserMediaPreferencesCommand implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $resolution,
|
||||
public string $codec,
|
||||
public string $language,
|
||||
public string $provider,
|
||||
) {}
|
||||
}
|
||||
50
src/User/Action/Handler/SaveUserMediaPreferencesHandler.php
Normal file
50
src/User/Action/Handler/SaveUserMediaPreferencesHandler.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Handler;
|
||||
|
||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||
use App\User\Action\Result\SaveUserMediaPreferencesResult;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/** @implements HandlerInterface<SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserMediaPreferencesHandler implements HandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly PreferencesRepository $preferenceRepository,
|
||||
private readonly Security $token,
|
||||
) {}
|
||||
|
||||
public function handle(C $command): R
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->token->getUser();
|
||||
|
||||
foreach ($command as $preference => $value) {
|
||||
if ($user->hasUserPreference($preference)) {
|
||||
$user->updateUserPreference($preference, $value);
|
||||
$this->entityManager->flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
$preference = $this->preferenceRepository->find($preference);
|
||||
$user->addUserPreference(
|
||||
(new UserPreference())
|
||||
->setUser($user)
|
||||
->setPreference($preference)
|
||||
->setPreferenceValue($value)
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new SaveUserMediaPreferencesResult($user->getUserPreferences());
|
||||
}
|
||||
}
|
||||
40
src/User/Action/Input/SaveUserMediaPreferencesInput.php
Normal file
40
src/User/Action/Input/SaveUserMediaPreferencesInput.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Input;
|
||||
|
||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||
use OneToMany\RichBundle\Contract\InputInterface;
|
||||
|
||||
/** @implements InputInterface<SaveUserMediaPreferencesInput, SaveUserMediaPreferencesCommand> */
|
||||
class SaveUserMediaPreferencesInput implements InputInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[SourceSecurity]
|
||||
public mixed $userId,
|
||||
|
||||
#[SourceRequest('resolution')]
|
||||
public string $resolution,
|
||||
|
||||
#[SourceRequest('codec')]
|
||||
public string $codec,
|
||||
|
||||
#[SourceRequest('language')]
|
||||
public string $language,
|
||||
|
||||
#[SourceRequest('provider')]
|
||||
public string $provider,
|
||||
) {}
|
||||
|
||||
public function toCommand(): C
|
||||
{
|
||||
return new SaveUserMediaPreferencesCommand(
|
||||
$this->resolution,
|
||||
$this->codec,
|
||||
$this->language,
|
||||
$this->provider,
|
||||
);
|
||||
}
|
||||
}
|
||||
14
src/User/Action/Result/SaveUserMediaPreferencesResult.php
Normal file
14
src/User/Action/Result/SaveUserMediaPreferencesResult.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Action\Result;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
|
||||
/** @implements ResultInterface */
|
||||
class SaveUserMediaPreferencesResult implements ResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Collection $userPreferences,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Controller\Api;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class PreferencesApiController extends AbstractController
|
||||
{
|
||||
#[Route('/api/preferences/media', name: 'api_preferences_media')]
|
||||
public function getMediaPreferences(
|
||||
PreferencesRepository $preferencesRepository,
|
||||
): Response {
|
||||
return $this->json(
|
||||
Map::from($preferencesRepository->findEnabled())
|
||||
->rekey(fn($element) => $element->getId())
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/User/Framework/Controller/Api/UserApiController.php
Normal file
20
src/User/Framework/Controller/Api/UserApiController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Controller\Api;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class UserApiController extends AbstractController
|
||||
{
|
||||
#[Route('/api/user/preferences/values', name: 'api_user_preferences_values')]
|
||||
public function getUserPreferenceValues(
|
||||
Security $security
|
||||
): Response {
|
||||
return $this->json(
|
||||
$security->getUser()->getUserPreferenceValues()
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/User/Framework/Controller/Web/LoginController.php
Normal file
32
src/User/Framework/Controller/Web/LoginController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Controller\Web;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class LoginController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/login', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('user/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'app_logout')]
|
||||
public function logout(): void
|
||||
{
|
||||
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||
}
|
||||
}
|
||||
90
src/User/Framework/Controller/Web/PreferencesController.php
Normal file
90
src/User/Framework/Controller/Web/PreferencesController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\User\Framework\Controller\Web;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use App\Util\CountryCodes;
|
||||
use App\Util\CountryLanguages;
|
||||
use App\Util\ProviderList;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class PreferencesController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PreferencesRepository $preferencesRepository,
|
||||
private readonly SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
#[Route('/media/preferences', 'app_media_preferences', methods: ['GET'])]
|
||||
public function mediaPreferences(): Response
|
||||
{
|
||||
$enabledPreferences = $this->preferencesRepository->findEnabled();
|
||||
|
||||
if ($this->security->getUser()->getUserPreferences()->count() !== count($enabledPreferences)) {
|
||||
$this->setUserPreferences($this->security->getUser(), $enabledPreferences);
|
||||
}
|
||||
|
||||
$userPreferences = $this->security->getUser()->getUserPreferences()->toArray();
|
||||
$userPreferences = Map::from($userPreferences)
|
||||
->rekey(fn($preference) => $preference->getPreference()->getId());
|
||||
|
||||
$languages = CountryLanguages::$languages;
|
||||
sort($languages);
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'preferences' => $this->preferencesRepository->findEnabled(),
|
||||
'languages' => $languages,
|
||||
'providers' => ProviderList::$providers,
|
||||
'userPreferences' => $userPreferences->toArray(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/media/preferences', 'app_save_media_preferences', methods: ['POST'])]
|
||||
public function saveMediaPreferences(
|
||||
SaveUserMediaPreferencesInput $input,
|
||||
): Response
|
||||
{
|
||||
$userPreferences = $this->saveUserMediaPreferencesHandler->handle($input->toCommand())->userPreferences;
|
||||
$userPreferences = Map::from($userPreferences)->rekey(fn($preference) => $preference->getPreference()->getId());
|
||||
|
||||
$languages = CountryLanguages::$languages;
|
||||
sort($languages);
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
'preferences' => $this->preferencesRepository->findEnabled(),
|
||||
'languages' => $languages,
|
||||
'providers' => ProviderList::$providers,
|
||||
'userPreferences' => $userPreferences->toArray(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function setUserPreferences(User $user, array $preferences): void
|
||||
{
|
||||
foreach ($preferences as $preference) {
|
||||
if (false === $user->hasUserPreference($preference->getId())) {
|
||||
$user->addUserPreference((new UserPreference())
|
||||
->setUser($user)
|
||||
->setPreference($preference)
|
||||
->setPreferenceValue(null)
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->preferencesRepository->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
61
src/User/Framework/Controller/Web/RegistrationController.php
Normal file
61
src/User/Framework/Controller/Web/RegistrationController.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Controller\Web;
|
||||
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Entity\UserPreference;
|
||||
use App\User\Framework\Form\RegistrationFormType;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class RegistrationController extends AbstractController
|
||||
{
|
||||
#[Route('/register', name: 'app_register')]
|
||||
public function register(
|
||||
Request $request,
|
||||
UserPasswordHasherInterface $userPasswordHasher,
|
||||
EntityManagerInterface $entityManager,
|
||||
PreferencesRepository $preferencesRepository,
|
||||
): Response {
|
||||
$user = new User();
|
||||
$form = $this->createForm(RegistrationFormType::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var string $plainPassword */
|
||||
$plainPassword = $form->get('plainPassword')->getData();
|
||||
|
||||
// encode the plain password
|
||||
$user->setPassword($userPasswordHasher->hashPassword($user, $plainPassword));
|
||||
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->setUserPreferences($user, $preferencesRepository->findEnabled());
|
||||
|
||||
$preferencesRepository->getEntityManager()->flush();
|
||||
|
||||
return $this->redirectToRoute('app_index');
|
||||
}
|
||||
|
||||
return $this->render('user/register.html.twig', [
|
||||
'registrationForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
private function setUserPreferences(User $user, array $preferences): void
|
||||
{
|
||||
foreach ($preferences as $preference) {
|
||||
$user->addUserPreference((new UserPreference())
|
||||
->setUser($user)
|
||||
->setPreference($preference)
|
||||
->setPreferenceValue(null)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/User/Framework/Entity/Preference.php
Normal file
107
src/User/Framework/Entity/Preference.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Entity;
|
||||
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PreferencesRepository::class)]
|
||||
class Preference
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $enabled = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, PreferenceOption>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: PreferenceOption::class, mappedBy: 'preference', fetch: 'EAGER')]
|
||||
private Collection $preferenceOptions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->preferenceOptions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEnabled(): ?bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, PreferenceOption>
|
||||
*/
|
||||
public function getPreferenceOptions(): Collection
|
||||
{
|
||||
return $this->preferenceOptions;
|
||||
}
|
||||
|
||||
public function addPreferenceOption(PreferenceOption $preferenceOption): static
|
||||
{
|
||||
if (!$this->preferenceOptions->contains($preferenceOption)) {
|
||||
$this->preferenceOptions->add($preferenceOption);
|
||||
$preferenceOption->setPreference($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePreferenceOption(PreferenceOption $preferenceOption): static
|
||||
{
|
||||
if ($this->preferenceOptions->removeElement($preferenceOption)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($preferenceOption->getPreference() === $this) {
|
||||
$preferenceOption->setPreference(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
82
src/User/Framework/Entity/PreferenceOption.php
Normal file
82
src/User/Framework/Entity/PreferenceOption.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Entity;
|
||||
|
||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PreferenceOptionRepository::class)]
|
||||
class PreferenceOption
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $value = null;
|
||||
|
||||
#[Ignore]
|
||||
#[ORM\ManyToOne(inversedBy: 'preferenceOptions')]
|
||||
private ?Preference $preference = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $enabled = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(?string $value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPreference(): ?Preference
|
||||
{
|
||||
return $this->preference;
|
||||
}
|
||||
|
||||
public function setPreference(?Preference $preference): static
|
||||
{
|
||||
$this->preference = $preference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEnabled(): ?bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
244
src/User/Framework/Entity/User.php
Normal file
244
src/User/Framework/Entity/User.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Entity;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
||||
private ?string $email;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $name;
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $password;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserPreference>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
|
||||
private Collection $userPreferences;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Monitor>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Monitor::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $yes;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userPreferences = new ArrayCollection();
|
||||
$this->yes = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public representation of the user (e.g. a username, an email address, etc.)
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserPreference>
|
||||
*/
|
||||
public function getUserPreferences(): Collection
|
||||
{
|
||||
return $this->userPreferences;
|
||||
}
|
||||
|
||||
public function getUserPreference(string $preferenceName)
|
||||
{
|
||||
foreach ($this->userPreferences as $userPreference) {
|
||||
if ($userPreference->getPreference()->getName() === $preferenceName) {
|
||||
return $userPreference->getPreference();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasUserPreference(string $preferenceName): bool
|
||||
{
|
||||
foreach ($this->userPreferences as $userPreference) {
|
||||
if ($userPreference->getPreference()->getId() === $preferenceName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updateUserPreference(string $preferenceName, mixed $preferenceValue): static
|
||||
{
|
||||
foreach ($this->userPreferences as $userPreference) {
|
||||
if ($userPreference->getPreference()->getId() === $preferenceName) {
|
||||
$userPreference->setPreferenceValue($preferenceValue);
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUserPreference(UserPreference $userPreference): static
|
||||
{
|
||||
if (!$this->userPreferences->contains($userPreference)) {
|
||||
$this->userPreferences->add($userPreference);
|
||||
$userPreference->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUserPreference(UserPreference $userPreference): static
|
||||
{
|
||||
if ($this->userPreferences->removeElement($userPreference)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($userPreference->getUser() === $this) {
|
||||
$userPreference->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserPreferenceValues()
|
||||
{
|
||||
return Map::from($this->userPreferences)
|
||||
->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId())
|
||||
->map(function (UserPreference $userPreference) {
|
||||
if (in_array($userPreference->getPreference()->getId(), ['language', 'provider'])) {
|
||||
return $userPreference->getPreferenceValue();
|
||||
}
|
||||
foreach ($userPreference->getPreference()->getPreferenceOptions() as $preferenceOption) {
|
||||
// dd((int) $userPreference->getPreferenceValue(), $preferenceOption->getId(), $preferenceOption->getValue());
|
||||
if ($preferenceOption->getId() === (int) $userPreference->getPreferenceValue()) {
|
||||
return $preferenceOption->getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Monitor>
|
||||
*/
|
||||
public function getYes(): Collection
|
||||
{
|
||||
return $this->yes;
|
||||
}
|
||||
|
||||
public function addYe(Monitor $ye): static
|
||||
{
|
||||
if (!$this->yes->contains($ye)) {
|
||||
$this->yes->add($ye);
|
||||
$ye->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeYe(Monitor $ye): static
|
||||
{
|
||||
if ($this->yes->removeElement($ye)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($ye->getUser() === $this) {
|
||||
$ye->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
67
src/User/Framework/Entity/UserPreference.php
Normal file
67
src/User/Framework/Entity/UserPreference.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Entity;
|
||||
|
||||
use App\User\Framework\Repository\UserPreferenceRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserPreferenceRepository::class)]
|
||||
class UserPreference
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'userPreferences')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\ManyToOne(fetch: 'EAGER')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Preference $preference = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $preference_value = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPreference(): ?Preference
|
||||
{
|
||||
return $this->preference;
|
||||
}
|
||||
|
||||
public function setPreference(?Preference $preference): static
|
||||
{
|
||||
$this->preference = $preference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPreferenceValue(): ?string
|
||||
{
|
||||
return $this->preference_value;
|
||||
}
|
||||
|
||||
public function setPreferenceValue(?string $preference_value): static
|
||||
{
|
||||
$this->preference_value = $preference_value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
46
src/User/Framework/Form/RegistrationFormType.php
Normal file
46
src/User/Framework/Form/RegistrationFormType.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Form;
|
||||
|
||||
use App\User\Framework\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class RegistrationFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email')
|
||||
->add('name')
|
||||
->add('plainPassword', PasswordType::class, [
|
||||
// instead of being set onto the object directly,
|
||||
// this is read and encoded in the controller
|
||||
'mapped' => false,
|
||||
'attr' => ['autocomplete' => 'new-password'],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Please enter a password',
|
||||
]),
|
||||
new Length([
|
||||
'min' => 6,
|
||||
'minMessage' => 'Your password should be at least {{ limit }} characters',
|
||||
// max length allowed by Symfony for security reasons
|
||||
'max' => 4096,
|
||||
]),
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
src/User/Framework/Repository/PreferenceOptionRepository.php
Normal file
43
src/User/Framework/Repository/PreferenceOptionRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Repository;
|
||||
|
||||
use App\User\Framework\Entity\PreferenceOption;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<PreferenceOption>
|
||||
*/
|
||||
class PreferenceOptionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, PreferenceOption::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return PreferenceOption[] Returns an array of PreferenceOption objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('p.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?PreferenceOption
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user