Compare commits
45 Commits
dev-redis
...
dev-style-
| Author | SHA1 | Date | |
|---|---|---|---|
| 46deba2982 | |||
| 74506f6928 | |||
| 845a67bdd8 | |||
| 651697640c | |||
| dd48cc542f | |||
| 6c0e42d291 | |||
| e230913c89 | |||
| 8967d407cb | |||
| 217a667df2 | |||
| 4653feb123 | |||
| 6ad10a585d | |||
| eed2e70d21 | |||
| eded5a2fc8 | |||
| 8428fc6cf6 | |||
| 888a030680 | |||
| a628d85ef2 | |||
| afb62645f6 | |||
| 8aba35fee1 | |||
| 6817bd8c80 | |||
| 854177a121 | |||
| ddb71b3bb0 | |||
| 35a3e48ac9 | |||
| 6e55195e6f | |||
| e325687af5 | |||
| 4506306377 | |||
| 4287b52bd4 | |||
| 3724bcbb16 | |||
| 6c2cd7510f | |||
| 98bf8d2880 | |||
| 20ade478b1 | |||
| 4eed5fef78 | |||
| 5ff9842eaa | |||
| b93da3df1d | |||
| fe0ab2ef5a | |||
| 25ff3e726d | |||
| 527adb73c1 | |||
| 9166b4bbc8 | |||
| 5688b3a0df | |||
| babcb00440 | |||
| 993b34d668 | |||
| 73b3e5179a | |||
| d3176baff2 | |||
| cc77cccf0b | |||
| b0c10a028a | |||
| 12bf90a2b4 |
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="* * * * *"
|
||||
|
||||
20
.env.dist
20
.env.dist
@@ -1,3 +1,4 @@
|
||||
APP_ENV=%%app_env%%
|
||||
APP_SECRET="%%app_secret%%"
|
||||
DATABASE_URL="%%db_url%%"
|
||||
DOWNLOAD_DIR=%%download_dir%%
|
||||
@@ -9,3 +10,22 @@ MERCURE_JWT_SECRET="%%mercure_jwt_secret%%"
|
||||
JELLYFIN_URL=%%jellyfin_url%%
|
||||
JELLYFIN_TOKEN=%%jellyfin_token%%
|
||||
REDIS_HOST="%%redis_host%%"
|
||||
|
||||
|
||||
|
||||
LDAP_HOST=
|
||||
LDAP_PORT=
|
||||
LDAP_ENCRYPTION=
|
||||
LDAP_BASE_DN=
|
||||
LDAP_BIND_USER=
|
||||
LDAP_BIND_PASS=
|
||||
LDAP_DN_STRING=
|
||||
LDAP_UID_KEY="uid"
|
||||
|
||||
# LDAP group that identifies an Admin
|
||||
# Users with this LDAP group will automatically
|
||||
# get the admin role in this system.
|
||||
LDAP_ADMIN_ROLE_DN="cn=admins,cn=groups,cn=accounts,dc=caldwell,dc=local"
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
LDAP_USERNAME_ATTRIBUTE=uid
|
||||
LDAP_NAME_ATTRIBUTE=displayname
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
FROM registry.caldwell.digital/library/php:8.4-apache
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install libldap2-dev -y && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||
docker-php-ext-install ldap
|
||||
|
||||
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
|
||||
RUN rm /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
FROM registry.caldwell.digital/library/php:8.4-apache
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install libldap2-dev -y && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||
docker-php-ext-install ldap
|
||||
|
||||
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
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
download() {
|
||||
fetch('/download', {
|
||||
fetch('/api/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -26,7 +26,7 @@ export default class extends Controller {
|
||||
title: this.element.dataset['title'],
|
||||
filename: this.filenameValue,
|
||||
mediaType: this.mediaTypeValue,
|
||||
imdbId: this.imdbIdValue
|
||||
imdbId: this.imdbIdValue,
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
|
||||
42
assets/controllers/download_list_controller.js
Normal file
42
assets/controllers/download_list_controller.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { getComponent } from '@symfony/ux-live-component';
|
||||
/*
|
||||
* 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 = ['download']
|
||||
|
||||
async initialize() {
|
||||
this.component = await getComponent(this.element);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
downloadTargetConnected(target) {
|
||||
let downloads = this.element.querySelectorAll('tbody tr');
|
||||
if (downloads.length > 5) {
|
||||
target.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,13 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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');
|
||||
|
||||
1
assets/icons/flowbite/download-outline.svg
Normal file
1
assets/icons/flowbite/download-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2m-1-5l-4 5l-4-5m9 8h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
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 |
38
build.xml
38
build.xml
@@ -1,14 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="Caldwell Digital Symfony Template" default="build">
|
||||
<project name="Torsearch" default="build">
|
||||
<!-- build dev for dev envs -->
|
||||
<target name="build" depends="setEnv,composer,compileAssets,migrateDb" />
|
||||
<target name="build" depends="setEnv,composer,compileAssets,migrateDb,clearCache" />
|
||||
|
||||
<target name="composer" description="Run composer">
|
||||
<exec executable="composer">
|
||||
<arg value="install" />
|
||||
<arg value="--ignore-platform-reqs" />
|
||||
</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,26 +26,6 @@
|
||||
</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="app_secret" value="${APP_SECRET}" />
|
||||
<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}" />
|
||||
<token key="redis_host" value="${REDIS_HOST}" />
|
||||
</replacetokens>
|
||||
</filterchain>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
<target name="migrateDb" description="Migrate the database">
|
||||
<exec executable="php">
|
||||
<arg value="bin/console" />
|
||||
@@ -47,4 +33,12 @@
|
||||
<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>
|
||||
|
||||
@@ -29,7 +29,14 @@ 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 -vvv --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
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"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",
|
||||
@@ -22,12 +24,16 @@
|
||||
"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/ldap": "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.*",
|
||||
@@ -93,6 +99,7 @@
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
|
||||
939
composer.lock
generated
939
composer.lock
generated
File diff suppressed because it is too large
Load Diff
56
config/dist/ldap.security.yaml
vendored
Normal file
56
config/dist/ldap.security.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_local:
|
||||
entity:
|
||||
class: App\User\Framework\Entity\User
|
||||
property: email
|
||||
|
||||
app_ldap:
|
||||
id: App\User\Framework\Security\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_ldap
|
||||
form_login_ldap:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
service: Symfony\Component\Ldap\Ldap
|
||||
dn_string: '%env(LDAP_DN_STRING)%'
|
||||
logout:
|
||||
path: app_logout
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# 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: ^/register, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
54
config/dist/local.security.yaml
vendored
Normal file
54
config/dist/local.security.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_local:
|
||||
entity:
|
||||
class: App\User\Framework\Entity\User
|
||||
property: email
|
||||
|
||||
app_ldap:
|
||||
id: App\User\Framework\Security\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_local
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: app_logout
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# 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: ^/register, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
@@ -30,6 +30,12 @@ doctrine:
|
||||
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,6 +2,10 @@
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
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' ]
|
||||
|
||||
@@ -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,26 +5,29 @@ security:
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
app_local:
|
||||
entity:
|
||||
class: App\User\Framework\Entity\User
|
||||
property: email
|
||||
|
||||
app_ldap:
|
||||
id: App\User\Framework\Security\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
provider: app_ldap
|
||||
form_login_ldap:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
service: Symfony\Component\Ldap\Ldap
|
||||
dn_string: '%env(LDAP_DN_STRING)%'
|
||||
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
|
||||
|
||||
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:
|
||||
@@ -13,3 +13,19 @@ controllersUser:
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: ['https']
|
||||
|
||||
controllersDownload:
|
||||
resource:
|
||||
path: ../src/Download/Framework/Controller
|
||||
namespace: App\Download\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: [ 'https' ]
|
||||
|
||||
controllersMonitor:
|
||||
resource:
|
||||
path: ../src/Monitor/Framework/Controller
|
||||
namespace: App\Monitor\Framework\Controller
|
||||
type: attribute
|
||||
defaults:
|
||||
schemes: ['https']
|
||||
|
||||
61
config/security.yaml
Normal file
61
config/security.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_local:
|
||||
entity:
|
||||
class: App\User\Framework\Entity\User
|
||||
property: email
|
||||
|
||||
app_ldap:
|
||||
id: App\User\Framework\Security\LdapUserProvider
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_ldap
|
||||
entry_point: form_login_ldap
|
||||
form_login_ldap:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
service: Symfony\Component\Ldap\Ldap
|
||||
dn_string: '%env(LDAP_DN_STRING)%'
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: app_logout
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# 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: ^/register, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
@@ -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
|
||||
@@ -24,6 +28,37 @@ services:
|
||||
# please note that last definitions always *replace* previous ones
|
||||
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
|
||||
|
||||
# Session
|
||||
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
|
||||
arguments:
|
||||
- '%env(DATABASE_URL)%'
|
||||
|
||||
# LDAP
|
||||
App\User\Framework\Security\LdapUserProvider:
|
||||
arguments:
|
||||
$userRepository: '@App\User\Framework\Repository\UserRepository'
|
||||
$ldap: '@Symfony\Component\Ldap\LdapInterface'
|
||||
$baseDn: '%env(LDAP_BASE_DN)%'
|
||||
$searchDn: '%env(LDAP_BIND_USER)%'
|
||||
$searchPassword: '%env(LDAP_BIND_PASS)%'
|
||||
$defaultRoles: ['ROLE_USER']
|
||||
$uidKey: '%env(LDAP_UID_KEY)%'
|
||||
# $passwordAttribute: '%env(LDAP_PASSWORD_ATTRIBUTE)%'
|
||||
|
||||
|
||||
Symfony\Component\Ldap\LdapInterface: '@Symfony\Component\Ldap\Ldap'
|
||||
|
||||
Symfony\Component\Ldap\Ldap:
|
||||
arguments: [ '@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter' ]
|
||||
tags:
|
||||
- ldap
|
||||
|
||||
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
|
||||
arguments:
|
||||
- host: '%env(LDAP_HOST)%'
|
||||
port: '%env(LDAP_PORT)%'
|
||||
encryption: '%env(LDAP_ENCRYPTION)%'
|
||||
options:
|
||||
protocol_version: 3
|
||||
referrals: false
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ services:
|
||||
deploy:
|
||||
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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
35
migrations/Version20250510185814.php
Normal file
35
migrations/Version20250510185814.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 Version20250510185814 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 username 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 username
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
47
migrations/Version20250511050008.php
Normal file
47
migrations/Version20250511050008.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 Version20250511050008 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 download ADD user_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download ADD CONSTRAINT FK_781A8270A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_781A8270A76ED395 ON download (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 download DROP FOREIGN KEY FK_781A8270A76ED395
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_781A8270A76ED395 ON download
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE download DROP user_id
|
||||
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/
|
||||
58
src/Command/ConfigSetCommand.php
Normal file
58
src/Command/ConfigSetCommand.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'config:set',
|
||||
description: 'Add a short description for your command',
|
||||
)]
|
||||
class ConfigSetCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('key', InputArgument::REQUIRED, 'Config key')
|
||||
->addArgument('value', InputArgument::REQUIRED, 'Config value')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$key = $input->getArgument('key');
|
||||
$handlers = [
|
||||
'auth.method' => 'setAuthMethod',
|
||||
];
|
||||
|
||||
$handler = $handlers[$key];
|
||||
$this->$handler($input, $io);
|
||||
|
||||
$io->success('Success: "' . $input->getArgument('key') . '" set to "' . $input->getArgument('value') . '"');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function setAuthMethod(InputInterface $input, SymfonyStyle $io)
|
||||
{
|
||||
$config = [
|
||||
'local' => 'config/dist/local.security.yaml',
|
||||
'ldap' => 'config/dist/ldap.security.yaml',
|
||||
];
|
||||
$authMethod = $input->getArgument('value');
|
||||
$io->text('> Setting auth method to: ' . $authMethod);
|
||||
copy($config[$authMethod], 'config/packages/security.yaml');
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ final class AlertController extends AbstractController
|
||||
{
|
||||
$update = new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('broadcast/Alert.html.twig', [
|
||||
$this->renderer->render('Alert.stream.html.twig', [
|
||||
'alert_id' => 1,
|
||||
'title' => 'Added to queue',
|
||||
'message' => 'This is a testy test!',
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Controller;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Tmdb\Tmdb;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -16,11 +17,13 @@ final class IndexController extends AbstractController
|
||||
) {}
|
||||
|
||||
#[Route('/', name: 'app_index')]
|
||||
public function index(): Response
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$request->getSession()->set('mercure_alert_topic', 'alerts_' . uniqid());
|
||||
|
||||
return $this->render('index/index.html.twig', [
|
||||
'active_downloads' => $this->downloadRepository->getActivePaginated(),
|
||||
'recent_downloads' => $this->downloadRepository->latest(5),
|
||||
'active_downloads' => $this->getUser()->getActiveDownloads(),
|
||||
'recent_downloads' => $this->getUser()->getDownloads(),
|
||||
'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,10 +33,9 @@ final class SearchController extends AbstractController
|
||||
|
||||
#[Route('/result/{mediaType}/{tmdbId}', name: 'app_search_result')]
|
||||
public function result(
|
||||
GetMediaInfoInput $getDownloadOptionsInput,
|
||||
GetMediaInfoInput $input,
|
||||
): Response {
|
||||
$result = $this->getMediaInfoHandler->handle($getDownloadOptionsInput->toCommand());
|
||||
|
||||
$result = $this->getMediaInfoHandler->handle($input->toCommand());
|
||||
return $this->render('search/result.html.twig', [
|
||||
'results' => $result,
|
||||
'filter' => [
|
||||
|
||||
@@ -6,34 +6,90 @@ 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\Request;
|
||||
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, Request $request): 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(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderer->render('Alert.stream.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,6 +15,7 @@ class DownloadMediaCommand implements CommandInterface
|
||||
public string $filename,
|
||||
public string $mediaType,
|
||||
public string $imdbId,
|
||||
public int $userId,
|
||||
public ?int $downloadId = null,
|
||||
) {}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Download\Action\Command\DownloadMediaCommand;
|
||||
use App\Download\Action\Result\DownloadMediaResult;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Download\Downloader\DownloaderInterface;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||
@@ -17,12 +18,14 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
||||
public function __construct(
|
||||
private DownloaderInterface $downloader,
|
||||
private DownloadRepository $downloadRepository,
|
||||
private UserRepository $userRepository,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
if (null === $command->downloadId) {
|
||||
$download = $this->downloadRepository->insert(
|
||||
$this->userRepository->find($command->userId),
|
||||
$command->url,
|
||||
$command->title,
|
||||
$command->filename,
|
||||
@@ -34,6 +37,7 @@ readonly class DownloadMediaHandler implements HandlerInterface
|
||||
$download = $this->downloadRepository->find($command->downloadId);
|
||||
}
|
||||
|
||||
dump($download);
|
||||
|
||||
try {
|
||||
$this->downloadRepository->updateStatus($download->getId(), 'In Progress');
|
||||
|
||||
@@ -26,6 +26,8 @@ class DownloadMediaInput implements InputInterface
|
||||
#[SourceRequest('imdbId')]
|
||||
public string $imdbId,
|
||||
|
||||
public ?int $userId = null,
|
||||
|
||||
public ?int $downloadId = null,
|
||||
) {}
|
||||
|
||||
@@ -37,6 +39,7 @@ class DownloadMediaInput implements InputInterface
|
||||
$this->filename,
|
||||
$this->mediaType,
|
||||
$this->imdbId,
|
||||
$this->userId,
|
||||
$this->downloadId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
namespace App\Download\Framework\Controller;
|
||||
|
||||
use App\Download\Action\Input\DownloadMediaInput;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class DownloadController extends AbstractController
|
||||
class ApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
private MessageBusInterface $bus,
|
||||
private readonly HubInterface $hub,
|
||||
) {}
|
||||
|
||||
#[Route('/download', name: 'app_download', methods: ['POST'])]
|
||||
#[Route('/api/download', name: 'api_download', methods: ['POST'])]
|
||||
public function download(
|
||||
Request $request,
|
||||
DownloadMediaInput $input,
|
||||
): Response {
|
||||
$download = $this->downloadRepository->insert(
|
||||
$this->getUser(),
|
||||
$input->url,
|
||||
$input->title,
|
||||
$input->filename,
|
||||
@@ -28,13 +34,26 @@ class DownloadController extends AbstractController
|
||||
$input->mediaType,
|
||||
"",
|
||||
);
|
||||
$this->downloadRepository->getEntityManager()->persist($download);
|
||||
$this->downloadRepository->getEntityManager()->flush();
|
||||
$input->downloadId = $download->getId();
|
||||
$input->userId = $this->getUser()->getId();
|
||||
|
||||
try {
|
||||
$this->bus->dispatch($input->toCommand());
|
||||
} catch (\Throwable $exception) {
|
||||
return $this->json(['error' => $exception->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderView('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => '"' . $input->title . '" added to Queue',
|
||||
])
|
||||
));
|
||||
|
||||
return $this->json(['status' => 200, 'message' => 'Added to Queue']);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Download/Framework/Controller/WebController.php
Normal file
27
src/Download/Framework/Controller/WebController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Download\Framework\Controller;
|
||||
|
||||
use App\Download\Action\Input\DownloadMediaInput;
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class WebController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
private MessageBusInterface $bus,
|
||||
private readonly HubInterface $hub,
|
||||
) {}
|
||||
|
||||
#[Route('/downloads', name: 'app_downloads', methods: ['GET'])]
|
||||
public function download(): Response {
|
||||
return $this->render('downloads/index.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Download\Framework\Entity;
|
||||
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\User\Framework\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||
|
||||
@@ -39,6 +40,9 @@ class Download
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $batchId = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'downloads')]
|
||||
private ?User $user = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -146,4 +150,16 @@ class Download
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Download\Framework\Repository;
|
||||
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use App\ValueObject\DownloadRequest;
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\Util\Paginator;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Knp\Component\Pager\Paginator;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Download>
|
||||
@@ -16,41 +16,43 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
{
|
||||
private ManagerRegistry $managerRegistry;
|
||||
|
||||
public function __construct(ManagerRegistry $registry, ManagerRegistry $managerRegistry)
|
||||
private Paginator $paginator;
|
||||
|
||||
public function __construct(ManagerRegistry $registry, ManagerRegistry $managerRegistry, Paginator $paginator)
|
||||
{
|
||||
parent::__construct($registry, Download::class);
|
||||
$this->managerRegistry = $managerRegistry;
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
public function getCompletePaginated(int $pageNumber = 1, int $perPage = 10)
|
||||
public function getCompletePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 10)
|
||||
{
|
||||
$firstResult = ($pageNumber - 1) * $perPage;
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->andWhere('d.user = :user')
|
||||
->orderBy('d.id', 'DESC')
|
||||
->setParameter('statuses', ['Complete'])
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($perPage)
|
||||
->setParameter('user', $user)
|
||||
->getQuery();
|
||||
|
||||
return new \Doctrine\ORM\Tools\Pagination\Paginator($query);
|
||||
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
||||
}
|
||||
|
||||
public function getActivePaginated(int $pageNumber = 1, int $perPage = 5)
|
||||
public function getActivePaginated(UserInterface $user, int $pageNumber = 1, int $perPage = 5)
|
||||
{
|
||||
$firstResult = ($pageNumber - 1) * $perPage;
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->andWhere('d.user = :user')
|
||||
->orderBy('d.id', 'ASC')
|
||||
->setParameter('statuses', ['New', 'In Progress'])
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($perPage)
|
||||
->setParameter('user', $user)
|
||||
->getQuery();
|
||||
|
||||
return new \Doctrine\ORM\Tools\Pagination\Paginator($query);
|
||||
return $this->paginator->paginate($query, $pageNumber, $perPage);
|
||||
}
|
||||
|
||||
public function insert(
|
||||
UserInterface $user,
|
||||
string $url,
|
||||
string $title,
|
||||
string $filename,
|
||||
@@ -59,7 +61,9 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
string $batchId,
|
||||
string $status = 'New'
|
||||
): Download {
|
||||
/** @var User $user */
|
||||
$download = (new Download())
|
||||
->setUser($user)
|
||||
->setUrl($url)
|
||||
->setTitle($title)
|
||||
->setFilename($filename)
|
||||
@@ -75,22 +79,6 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
return $download;
|
||||
}
|
||||
|
||||
public function insertFromDownloadRequest(DownloadRequest $request): Download
|
||||
{
|
||||
$download = (new Download())
|
||||
->setUrl($request->downloadUrl)
|
||||
->setTitle($request->seriesName)
|
||||
->setFilename($request->filename)
|
||||
->setImdbId($request->imdbCode)
|
||||
->setMediaType($request->mediaType)
|
||||
->setStatus('New');
|
||||
|
||||
$this->getEntityManager()->persist($download);
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
return $download;
|
||||
}
|
||||
|
||||
public function updateStatus(int $id, string $status): Download
|
||||
{
|
||||
$download = $this->find($id);
|
||||
@@ -106,18 +94,6 @@ class DownloadRepository extends ServiceEntityRepository
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
public function getPendingByBatchId(string $batchId): ?array
|
||||
{
|
||||
$query = $this->createQueryBuilder('d')
|
||||
->andWhere('d.status IN (:statuses)')
|
||||
->andWhere('d.batchId = :batchId')
|
||||
->setParameter('statuses', ['New', 'In Progress'])
|
||||
->setParameter('batchId', $batchId)
|
||||
->getQuery();
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
public function latest(int $limit = 1)
|
||||
{
|
||||
return $this->createQueryBuilder('d')
|
||||
|
||||
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 $userId,
|
||||
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->find($command->userId);
|
||||
$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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
77
src/Monitor/Action/Handler/MonitorMovieHandler.php
Normal file
77
src/Monitor/Action/Handler/MonitorMovieHandler.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\Entity\Monitor;
|
||||
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\Bundle\SecurityBundle\Security;
|
||||
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,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): ResultInterface
|
||||
{
|
||||
$this->logger->info('> [MonitorMovieHandler] Executing MonitorMovieHandler');
|
||||
/** @var Monitor $monitor */
|
||||
$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->getUser()->getId(),
|
||||
));
|
||||
$monitor->setStatus('Complete');
|
||||
$monitor->setDownloadedAt(new DateTimeIMmutable());
|
||||
} else {
|
||||
$monitor->setStatus('Active');
|
||||
}
|
||||
|
||||
$monitor->setLastSearch(new DateTimeImmutable());
|
||||
$monitor->incrementSearchCount();
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new MonitorMovieResult(
|
||||
status: 'OK',
|
||||
result: [
|
||||
'monitor' => $monitor,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
81
src/Monitor/Action/Handler/MonitorTvEpisodeHandler.php
Normal file
81
src/Monitor/Action/Handler/MonitorTvEpisodeHandler.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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->getUser()->getId(),
|
||||
));
|
||||
$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 int|string $userId,
|
||||
|
||||
#[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->userId,
|
||||
$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,
|
||||
) {}
|
||||
}
|
||||
48
src/Monitor/Framework/Controller/ApiController.php
Normal file
48
src/Monitor/Framework/Controller/ApiController.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Bundle\SecurityBundle\Security;
|
||||
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,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
#[Route('/api/monitor', name: 'api_monitor', methods: ['POST'])]
|
||||
public function addMonitor(
|
||||
AddMonitorInput $input,
|
||||
AddMonitorHandler $handler,
|
||||
HubInterface $hub,
|
||||
) {
|
||||
$command = $input->toCommand();
|
||||
$command->userId = $this->security->getUser()->getId();
|
||||
$response = $handler->handle($command);
|
||||
|
||||
$hub->publish(new Update(
|
||||
'alerts',
|
||||
$this->renderer->render('Alert.stream.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
36
src/Monitor/Framework/Repository/MonitorRepository.php
Normal file
36
src/Monitor/Framework/Repository/MonitorRepository.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Monitor\Framework\Repository;
|
||||
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\Util\Paginator;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Monitor>
|
||||
*/
|
||||
class MonitorRepository extends ServiceEntityRepository
|
||||
{
|
||||
private Paginator $paginator;
|
||||
|
||||
public function __construct(ManagerRegistry $registry, Paginator $paginator)
|
||||
{
|
||||
parent::__construct($registry, Monitor::class);
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
public function getUserMonitorsPaginated(UserInterface $user, int $page, int $perPage)
|
||||
{
|
||||
$query = $this->createQueryBuilder('m')
|
||||
->andWhere('m.status IN (:statuses)')
|
||||
->andWhere('m.user = :user')
|
||||
->orderBy('m.id', 'ASC')
|
||||
->setParameter('statuses', ['Active', 'New', 'In Progress', 'Complete'])
|
||||
->setParameter('user', $user)
|
||||
->getQuery();
|
||||
|
||||
return $this->paginator->paginate($query, $page, $perPage);
|
||||
}
|
||||
}
|
||||
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('* * * * *', 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,
|
||||
'tvshows' => 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;
|
||||
@@ -210,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,
|
||||
|
||||
@@ -8,6 +8,14 @@ use Nihilarr\PTN;
|
||||
|
||||
class ResultFactory
|
||||
{
|
||||
public static $codecMap = [
|
||||
'h264' => 'h264',
|
||||
'h265' => 'h265',
|
||||
'x264' => 'h264',
|
||||
'x265' => 'h265',
|
||||
'-' => '-'
|
||||
];
|
||||
|
||||
public static function map(
|
||||
string $url,
|
||||
string $title,
|
||||
@@ -25,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 ?? "-",
|
||||
@@ -101,6 +109,11 @@ class ResultFactory
|
||||
}
|
||||
}
|
||||
|
||||
public static function setCodec(string $codec): string
|
||||
{
|
||||
return self::$codecMap[strtolower($codec)] ?? $codec;
|
||||
}
|
||||
|
||||
private static function setEpisode(string $title)
|
||||
{
|
||||
$value = [];
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
|
||||
#[AsLiveComponent]
|
||||
final class ActiveDownloadList
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
|
||||
public function __construct(
|
||||
private DownloadRepository $downloadRepository,
|
||||
) {}
|
||||
|
||||
#[LiveAction]
|
||||
public function getDownloads()
|
||||
{
|
||||
return $this->downloadRepository->getActivePaginated();
|
||||
}
|
||||
}
|
||||
51
src/Twig/Components/DownloadList.php
Normal file
51
src/Twig/Components/DownloadList.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Download\Framework\Repository\DownloadRepository;
|
||||
use App\Util\Paginator;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveArg;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
|
||||
#[AsLiveComponent]
|
||||
final class DownloadList extends AbstractController
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public string $type;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public int $pageNumber = 1;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public int $perPage = 5;
|
||||
|
||||
#[LiveProp(writable: true)]
|
||||
public bool $isWidget = true;
|
||||
|
||||
public function __construct(
|
||||
private readonly DownloadRepository $downloadRepository,
|
||||
) {}
|
||||
|
||||
public function getDownloads()
|
||||
{
|
||||
if ($this->type === "active") {
|
||||
return $this->downloadRepository->getActivePaginated($this->getUser(), $this->pageNumber, $this->perPage);
|
||||
} elseif ($this->type === "complete") {
|
||||
return $this->downloadRepository->getCompletePaginated($this->getUser(), $this->pageNumber, $this->perPage);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function paginate(#[LiveArg] int $page)
|
||||
{
|
||||
$this->pageNumber = $page;
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,8 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
#[AsTwigComponent]
|
||||
final class Header
|
||||
{
|
||||
public function getActiveDownloads()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
25
src/Twig/Components/MonitorList.php
Normal file
25
src/Twig/Components/MonitorList.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveArg;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent]
|
||||
final class MonitorList extends AbstractController
|
||||
{
|
||||
public bool $isWidget = true;
|
||||
|
||||
public function __construct(
|
||||
private readonly MonitorRepository $monitorRepository,
|
||||
) {}
|
||||
|
||||
#[LiveAction]
|
||||
public function getUserMonitors(#[LiveArg] int $page = 1, #[LiveArg] int $perPage = 5)
|
||||
{
|
||||
return $this->monitorRepository->getUserMonitorsPaginated($this->getUser(), $page, $perPage);
|
||||
}
|
||||
}
|
||||
10
src/Twig/Components/StatusBadge.php
Normal file
10
src/Twig/Components/StatusBadge.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent]
|
||||
final class StatusBadge
|
||||
{
|
||||
}
|
||||
@@ -15,7 +15,10 @@ use App\Util\CountryLanguages;
|
||||
use App\Util\ProviderList;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class PreferencesController extends AbstractController
|
||||
@@ -24,6 +27,7 @@ class PreferencesController extends AbstractController
|
||||
private readonly PreferencesRepository $preferencesRepository,
|
||||
private readonly SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler,
|
||||
private readonly Security $security,
|
||||
private readonly HubInterface $hub,
|
||||
) {}
|
||||
#[Route('/media/preferences', 'app_media_preferences', methods: ['GET'])]
|
||||
public function mediaPreferences(): Response
|
||||
@@ -54,6 +58,7 @@ class PreferencesController extends AbstractController
|
||||
|
||||
#[Route('/media/preferences', 'app_save_media_preferences', methods: ['POST'])]
|
||||
public function saveMediaPreferences(
|
||||
Request $request,
|
||||
SaveUserMediaPreferencesInput $input,
|
||||
): Response
|
||||
{
|
||||
@@ -63,6 +68,15 @@ class PreferencesController extends AbstractController
|
||||
$languages = CountryLanguages::$languages;
|
||||
sort($languages);
|
||||
|
||||
$this->hub->publish(new Update(
|
||||
$request->getSession()->get('mercure_alert_topic'),
|
||||
$this->renderView('broadcast/Alert.stream.html.twig', [
|
||||
'alert_id' => uniqid(),
|
||||
'title' => 'Success',
|
||||
'message' => 'Your media preferences have been saved.',
|
||||
])
|
||||
));
|
||||
|
||||
return $this->render(
|
||||
'user/preferences.html.twig',
|
||||
[
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\User\Framework\Entity;
|
||||
|
||||
use Aimeos\Map;
|
||||
use App\User\Framework\Repository\PreferencesRepository;
|
||||
use App\Download\Framework\Entity\Download;
|
||||
use App\Monitor\Framework\Entity\Monitor;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -22,6 +23,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $username;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
||||
private ?string $email;
|
||||
|
||||
@@ -40,9 +44,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[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 $monitors;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Download>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Download::class, mappedBy: 'user')]
|
||||
private Collection $downloads;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userPreferences = new ArrayCollection();
|
||||
$this->monitors = new ArrayCollection();
|
||||
$this->downloads = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -81,7 +99,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
return (string) $this->username ?? $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,4 +222,94 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Monitor>
|
||||
*/
|
||||
public function getMonitors(): Collection
|
||||
{
|
||||
return $this->monitors;
|
||||
}
|
||||
|
||||
public function addMonitor(Monitor $monitor): static
|
||||
{
|
||||
if (!$this->monitors->contains($monitor)) {
|
||||
$this->monitors->add($monitor);
|
||||
$monitor->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMonitor(Monitor $monitor): static
|
||||
{
|
||||
if ($this->monitors->removeElement($monitor)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($monitor->getUser() === $this) {
|
||||
$monitor->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(?string $username): static
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Download>
|
||||
*/
|
||||
public function getDownloads(): Collection
|
||||
{
|
||||
return $this->downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Download>
|
||||
*/
|
||||
public function getActiveDownloads(): Collection
|
||||
{
|
||||
return $this->downloads->filter(fn(Download $download) => in_array($download->getStatus(), ['New', 'In Progress']));
|
||||
}
|
||||
|
||||
public function addDownload(Download $download): static
|
||||
{
|
||||
if (!$this->downloads->contains($download)) {
|
||||
$this->downloads->add($download);
|
||||
$download->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDownload(Download $download): static
|
||||
{
|
||||
if ($this->downloads->removeElement($download)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($download->getUser() === $this) {
|
||||
$download->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function queryDownloads(string $type = 'complete', int $limit = 5)
|
||||
{
|
||||
if ($type === 'complete') {
|
||||
return $this->downloads->filter(fn($item) => in_array($item->getStatus(), ['Complete']))->slice(0, $limit);
|
||||
} elseif ($type === 'in-progress') {
|
||||
return $this->downloads->filter(fn($item) => in_array($item->getStatus(), ['New', 'In Progress']))->slice(0, $limit);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
73
src/User/Framework/Security/LdapAuthenticator.php
Normal file
73
src/User/Framework/Security/LdapAuthenticator.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\User\Framework\Security;
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/doc/current/security/custom_authenticator.html
|
||||
*/
|
||||
class LdapAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
/**
|
||||
* Called on every request to decide if this authenticator should be
|
||||
* used for the request. Returning `false` will cause this authenticator
|
||||
* to be skipped.
|
||||
*/
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// return $request->headers->has('X-AUTH-TOKEN');
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
// $apiToken = $request->headers->get('X-AUTH-TOKEN');
|
||||
// if (null === $apiToken) {
|
||||
// The token header was empty, authentication fails with HTTP Status
|
||||
// Code 401 "Unauthorized"
|
||||
// throw new CustomUserMessageAuthenticationException('No API token provided');
|
||||
// }
|
||||
|
||||
// implement your own logic to get the user identifier from `$apiToken`
|
||||
// e.g. by looking up a user in the database using its API key
|
||||
// $userIdentifier = /** ... */;
|
||||
|
||||
// return new SelfValidatingPassport(new UserBadge($userIdentifier));
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
// on success, let the request continue
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$data = [
|
||||
// you may want to customize or obfuscate the message first
|
||||
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
|
||||
|
||||
// or to translate this message
|
||||
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
|
||||
];
|
||||
|
||||
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
// {
|
||||
// /*
|
||||
// * If you would like this class to control what happens when an anonymous user accesses a
|
||||
// * protected page (e.g. redirect to /login), uncomment this method and make this class
|
||||
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface.
|
||||
// *
|
||||
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
|
||||
// */
|
||||
// }
|
||||
}
|
||||
212
src/User/Framework/Security/LdapUserProvider.php
Normal file
212
src/User/Framework/Security/LdapUserProvider.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\User\Framework\Security;
|
||||
|
||||
use App\User\Framework\Entity\User;
|
||||
use App\User\Framework\Repository\UserRepository;
|
||||
use Symfony\Component\Ldap\Entry;
|
||||
use Symfony\Component\Ldap\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
|
||||
use Symfony\Component\Ldap\Exception\InvalidSearchCredentialsException;
|
||||
use Symfony\Component\Ldap\LdapInterface;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Ldap\Security\LdapUser;
|
||||
|
||||
/**
|
||||
* LdapUserProvider is a simple user provider on top of LDAP.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Charles Sarrazin <charles@sarraz.in>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @template-implements UserProviderInterface<LdapUser>
|
||||
*/
|
||||
class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
private string $uidKey;
|
||||
private string $defaultSearch;
|
||||
private string $usernameAttribute;
|
||||
private string $emailAttribute;
|
||||
private string $displayNameAttribute;
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private LdapInterface $ldap,
|
||||
private string $baseDn,
|
||||
private ?string $searchDn = null,
|
||||
#[\SensitiveParameter] private ?string $searchPassword = null,
|
||||
private array $defaultRoles = [],
|
||||
?string $uidKey = null,
|
||||
?string $filter = null,
|
||||
private ?string $passwordAttribute = null,
|
||||
private array $extraFields = [],
|
||||
string $usernameAttribute = 'uid',
|
||||
string $emailAttribute = 'mail',
|
||||
string $displayNameAttribute = 'displayName',
|
||||
) {
|
||||
$uidKey ??= 'sAMAccountName';
|
||||
$filter ??= '({uid_key}={user_identifier})';
|
||||
|
||||
$this->uidKey = $uidKey;
|
||||
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
|
||||
$this->usernameAttribute = $usernameAttribute;
|
||||
$this->emailAttribute = $emailAttribute;
|
||||
$this->displayNameAttribute = $displayNameAttribute;
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
try {
|
||||
$this->ldap->bind($this->searchDn, $this->searchPassword);
|
||||
} catch (InvalidCredentialsException) {
|
||||
throw new InvalidSearchCredentialsException();
|
||||
}
|
||||
|
||||
$identifier = $this->ldap->escape($identifier, '', LdapInterface::ESCAPE_FILTER);
|
||||
$query = str_replace('{user_identifier}', $identifier, $this->defaultSearch);
|
||||
$search = $this->ldap->query($this->baseDn, $query, ['filter' => 0 == \count($this->extraFields) ? '*' : $this->extraFields]);
|
||||
|
||||
$entries = $search->execute();
|
||||
$count = \count($entries);
|
||||
|
||||
if (!$count) {
|
||||
$e = new UserNotFoundException(\sprintf('User "%s" not found.', $identifier));
|
||||
$e->setUserIdentifier($identifier);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($count > 1) {
|
||||
$e = new UserNotFoundException('More than one user found.');
|
||||
$e->setUserIdentifier($identifier);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$entry = $entries[0];
|
||||
|
||||
try {
|
||||
$identifier = $this->getAttributeValue($entry, $this->uidKey);
|
||||
} catch (InvalidArgumentException) {
|
||||
// This is empty
|
||||
}
|
||||
|
||||
return $this->loadUser($identifier, $entry);
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
if (!$user instanceof LdapUser) {
|
||||
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||
}
|
||||
|
||||
return new LdapUser($user->getEntry(), $user->getUserIdentifier(), $user->getPassword(), $user->getRoles(), $user->getExtraFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!$user instanceof LdapUser) {
|
||||
throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
|
||||
}
|
||||
|
||||
if (null === $this->passwordAttribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$user->getEntry()->setAttribute($this->passwordAttribute, [$newHashedPassword]);
|
||||
$this->ldap->getEntryManager()->update($user->getEntry());
|
||||
$user->setPassword($newHashedPassword);
|
||||
} catch (ExceptionInterface) {
|
||||
// ignore failed password upgrades
|
||||
}
|
||||
}
|
||||
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return User::class === $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a user from an LDAP entry.
|
||||
*/
|
||||
protected function loadUser(string $identifier, Entry $entry): UserInterface
|
||||
{
|
||||
$extraFields = [];
|
||||
|
||||
foreach ($this->extraFields as $field) {
|
||||
$extraFields[$field] = $this->getAttributeValue($entry, $field);
|
||||
}
|
||||
|
||||
$dbUser = $this->getDbUser($identifier, $entry);
|
||||
|
||||
if (null === $dbUser) {
|
||||
$dbUser = new User();
|
||||
$dbUser->setPassword("test");
|
||||
}
|
||||
|
||||
$dbUser
|
||||
->setName( $this->getAttributeValue($entry, $this->displayNameAttribute)[0] ?? null)
|
||||
->setEmail($this->getAttributeValue($entry, $this->emailAttribute)[0] ?? null)
|
||||
->setUsername($this->getAttributeValue($entry, $this->usernameAttribute) ?? null);
|
||||
|
||||
$this->userRepository->getEntityManager()->persist($dbUser);
|
||||
$this->userRepository->getEntityManager()->flush();
|
||||
|
||||
return $dbUser;
|
||||
}
|
||||
|
||||
private function getDbUser(string $identifier, Entry $entry): ?UserInterface
|
||||
{
|
||||
if (in_array($this->uidKey, ['mail', 'email'])) {
|
||||
$dbUser = $this->userRepository->findOneBy(['email' => $identifier]);
|
||||
} else {
|
||||
$dbUser = $this->userRepository->findOneBy(['username' => $identifier]);
|
||||
}
|
||||
|
||||
// Attempt to map LDAP user to existing user
|
||||
if (null === $dbUser) {
|
||||
if ($entry->hasAttribute($this->emailAttribute)) {
|
||||
$dbUser = $this->userRepository->findOneBy(['email' => $this->getAttributeValue($entry, $this->emailAttribute)]);;
|
||||
}
|
||||
}
|
||||
|
||||
return $dbUser;
|
||||
}
|
||||
|
||||
private function getAttributeValue(Entry $entry, string $attribute): mixed
|
||||
{
|
||||
if (!$entry->hasAttribute($attribute)) {
|
||||
throw new InvalidArgumentException(\sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
|
||||
}
|
||||
|
||||
$values = $entry->getAttribute($attribute);
|
||||
if (!\in_array($attribute, [$this->uidKey, $this->passwordAttribute])) {
|
||||
return $values;
|
||||
}
|
||||
|
||||
if (1 !== \count($values)) {
|
||||
throw new InvalidArgumentException(\sprintf('Attribute "%s" has multiple values.', $attribute));
|
||||
}
|
||||
|
||||
return $values[0];
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class CountryLanguages
|
||||
'Filipino'
|
||||
];
|
||||
|
||||
public static function fromCountryCode(string $countryCode): string
|
||||
public static function fromCountryCode(string $countryCode): ?string
|
||||
{
|
||||
$countryLanguages = [
|
||||
'US' => 'English (US)',
|
||||
@@ -85,10 +85,10 @@ class CountryLanguages
|
||||
'KE' => 'English', // Also Swahili
|
||||
];
|
||||
|
||||
return $countryLanguages[$countryCode];
|
||||
return $countryLanguages[$countryCode] ?? null;
|
||||
}
|
||||
|
||||
public static function fromCountryName(string $countryName): string
|
||||
public static function fromCountryName(string $countryName): ?string
|
||||
{
|
||||
$countryLanguages = [
|
||||
'United States' => 'English (US)',
|
||||
@@ -135,6 +135,6 @@ class CountryLanguages
|
||||
'Kenya' => 'English', // Also Swahili
|
||||
];
|
||||
|
||||
return $countryLanguages[$countryName];
|
||||
return $countryLanguages[$countryName] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
62
src/Util/Paginator.php
Normal file
62
src/Util/Paginator.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator as OrmPaginator;
|
||||
|
||||
class Paginator
|
||||
{
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $total;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $lastPage;
|
||||
|
||||
private $items;
|
||||
|
||||
public $currentPage = 1;
|
||||
|
||||
/**
|
||||
* @param QueryBuilder|Query $query
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
* @return Paginator
|
||||
*/
|
||||
public function paginate($query, int $page = 1, int $limit = 5): Paginator
|
||||
{
|
||||
$paginator = new OrmPaginator($query);
|
||||
|
||||
$paginator
|
||||
->getQuery()
|
||||
->setFirstResult($limit * ($page - 1))
|
||||
->setMaxResults($limit);
|
||||
|
||||
$this->total = $paginator->count();
|
||||
$this->lastPage = (int) ceil($paginator->count() / $paginator->getQuery()->getMaxResults());
|
||||
$this->items = $paginator;
|
||||
$this->currentPage = $page;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotal(): int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function getLastPage(): int
|
||||
{
|
||||
return $this->lastPage;
|
||||
}
|
||||
|
||||
public function getItems()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
37
symfony.lock
37
symfony.lock
@@ -41,6 +41,18 @@
|
||||
"config/packages/http_discovery.yaml"
|
||||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
|
||||
},
|
||||
"files": [
|
||||
"phpstan.dist.neon"
|
||||
]
|
||||
},
|
||||
"symfony/asset-mapper": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
@@ -158,6 +170,18 @@
|
||||
"config/routes.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/scheduler": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "caea3c928ee9e1b21288fd76aef36f16ea355515"
|
||||
},
|
||||
"files": [
|
||||
"src/Schedule.php"
|
||||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
@@ -186,6 +210,19 @@
|
||||
"assets/controllers/hello_controller.js"
|
||||
]
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/translation.yaml",
|
||||
"translations/.gitignore"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
|
||||
@@ -12,15 +12,14 @@
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="bg-neutral-700 flex flex-col">
|
||||
<twig:Header />
|
||||
|
||||
<body class="bg-neutral-700 flex flex-col backdrop-filter backdrop-blur-sm bg-opacity-100">
|
||||
<div class="grid grid-cols-6">
|
||||
<div class="col-span-1">
|
||||
<div class="col-span-1 h-screen">
|
||||
<twig:NavBar />
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
<h2 class="p-4 mb-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||
<div class="col-span-5 h-screen overflow-y-scroll">
|
||||
<twig:Header />
|
||||
<h2 class="px-4 my-2 text-3xl font-bold text-gray-50">{% block h2 %}{% endblock %}</h2>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
{# Learn how to use Turbo Streams: https://github.com/symfony/ux-turbo#broadcast-doctrine-entities-update #}
|
||||
{% block create %}
|
||||
<turbo-stream action="remove" target="active_downloads_no_downloads">
|
||||
</turbo-stream>
|
||||
|
||||
<turbo-stream action="append" target="active_downloads">
|
||||
<template>
|
||||
<tr id="ad_download_{{ entity.id }}">
|
||||
<tr data-download-list-target="download" id="ad_download_{{ entity.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ entity.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<span class="p-1.5 bg-purple-600 rounded-full">
|
||||
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
|
||||
</span>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
|
||||
{% if entity.progress < 100 %}
|
||||
<div class="w-[3.25ch] h-[3.25ch] bg-purple-600 rounded-full block text-center table-cell align-middle text-xs text-gray-50">
|
||||
{{ entity.progress }}
|
||||
</div>
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="Complete" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
|
||||
<turbo-stream action="prepend" target="alert_list">
|
||||
<template>
|
||||
<twig:Alert title="Success" message="{{ entity.title }} has been added to the Download queue" alert_id="{{ entity.id }}" data-controller="alert" />
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
||||
|
||||
{% block update %}
|
||||
@@ -31,12 +32,15 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<span class="p-1.5 bg-purple-600 rounded-full">
|
||||
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
|
||||
<span class="mw-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% else %}
|
||||
<turbo-stream action="remove" target="complete_downloads_no_downloads">
|
||||
</turbo-stream>
|
||||
|
||||
<turbo-stream action="remove" target="ad_download_{{ id }}">
|
||||
</turbo-stream>
|
||||
|
||||
@@ -46,16 +50,14 @@
|
||||
</template>
|
||||
</turbo-stream>
|
||||
|
||||
<turbo-stream action="prepend" target="recent_downloads">
|
||||
<turbo-stream action="prepend" target="complete_downloads">
|
||||
<template>
|
||||
<tr id="recent_download_{{ entity.id }}">
|
||||
<tr id="ad_download_{{ entity.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ entity.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<span class="p-1.5 bg-purple-600 rounded-full">
|
||||
<span class="w-4 inline-block text-center text-gray-50">{{ entity.progress }}</span>
|
||||
</span>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-50">
|
||||
<twig:StatusBadge color="green" status="Complete" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -65,5 +67,4 @@
|
||||
|
||||
{% block remove %}
|
||||
<turbo-stream action="remove" target="ad_download_{{ id }}"></turbo-stream>
|
||||
{# <turbo-stream action="remove" target="cd_download_{{ id }}"></turbo-stream>#}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<div{{ attributes }} class="min-w-48">
|
||||
<table id="active_downloads" class="divide-y divide-gray-200 dark:divide-gray-50 dark:bg-gray-50 table-fixed" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
|
||||
<thead>
|
||||
<tr class="dark:bg-gray-50">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 min-w-[55ch] max-w-[55ch] truncate">
|
||||
Title
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
|
||||
Progress
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-50">
|
||||
{% if this.getDownloads()|length > 0 %}
|
||||
{% for download in this.getDownloads() %}
|
||||
<tr id="ad_download_{{ download.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 min-w-[45ch] max-w-[45ch] truncate">
|
||||
{{ download.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50">
|
||||
<span class="p-1.5 bg-purple-600 rounded-full">
|
||||
<span class="w-4 inline-block text-center text-gray-50">{{ download.progress }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-gray-800 dark:text-stone-800" colspan="2">
|
||||
No active downloads
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<li {{ attributes }} id="alert_{{ alert_id }}" class="alert p-4 text-green-800 border border-green-300 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400 dark:border-green-800" role="alert">
|
||||
<li {{ attributes }} id="alert_{{ alert_id }}" class="
|
||||
text-white bg-green-950 text-sm min-w-[250px]
|
||||
hover:bg-green-900 border border-green-500 px-4 py-3
|
||||
rounded-md z-40"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<div{{ attributes }}>
|
||||
<div class="flex flex-col bg-white border border-gray-200 border-t-4 border-t-orange-500 shadow-2xs rounded-xl dark:bg-slate-600 dark:border-neutral-700 dark:border-t-orange-500 dark:shadow-neutral-700/70">
|
||||
<div class="flex flex-col bg-sky-950 border-neutral-700 border-t-4 border-t-orange-500 rounded-xl
|
||||
backdrop-filter backdrop-blur-md bg-opacity-40
|
||||
">
|
||||
<div class="p-4 md:p-5">
|
||||
<h3 class="mb-4 text-lg font-bold text-gray-800 dark:text-white">
|
||||
<h3 class="mb-4 text-lg font-bold text-white">
|
||||
{{ title }}
|
||||
</h3>
|
||||
|
||||
|
||||
80
templates/components/DownloadList.html.twig
Normal file
80
templates/components/DownloadList.html.twig
Normal file
@@ -0,0 +1,80 @@
|
||||
<div{{ attributes.defaults(stimulus_controller('download_list')) }} class="min-w-48" >
|
||||
{% set table_body_id = (type == "complete") ? "complete_downloads" : "active_downloads" %}
|
||||
<table id="downloads" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-auto w-full" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
|
||||
<thead>
|
||||
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate">
|
||||
Title
|
||||
</th>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
|
||||
Filename
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-stone-500 uppercase dark:text-stone-800 truncate">
|
||||
Media type
|
||||
</th>
|
||||
{% endif %}
|
||||
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-stone-800">
|
||||
Progress
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="{{ table_body_id }}" class="divide-y divide-gray-200 dark:divide-gray-50">
|
||||
{% if this.downloads.items|length > 0 %}
|
||||
{% for download in this.downloads.items %}
|
||||
<tr id="ad_download_{{ download.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 {% if this.isWidget == true %}min-w-[45ch] max-w-[45ch]{% endif %} truncate">
|
||||
{{ download.title }}
|
||||
</td>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
{{ download.filename }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||
{{ download.mediaType }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
|
||||
{% if download.progress < 100 %}
|
||||
<div class="w-[3.25ch] h-[3.25ch] bg-purple-600 rounded-full block text-center table-cell align-middle text-xs text-gray-50">
|
||||
{{ download.progress }}
|
||||
</div>
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="Completed" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if this.isWidget == true and this.downloads.items|length > this.perPage %}
|
||||
<tr id="download_view_all">
|
||||
<td class="py-2 whitespace-nowrap bg-orange-500 uppercase text-sm font-medium text-center text-white truncate" colspan="100%">
|
||||
<a href="{{ path('app_downloads') }}">View All Downloads</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr id="{{ table_body_id }}_no_downloads">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center font-medium text-gray-800 dark:text-stone-800" colspan="100%">
|
||||
No downloads
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
{% if this.downloads.items|length > 0 %}
|
||||
{% set paginator = this.downloads %}
|
||||
{% include 'partial/paginator.html.twig' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
{% if results.media.mediaType == "tvshows" %}
|
||||
<div class="flex flex-row gap-2 justify-end px-8">
|
||||
<button class="px-1.5 py-1 bg-green-600 rounded-md text-sm"
|
||||
<button class="px-1.5 py-1 bg-green-600 hover:bg-green-700 rounded-md text-sm"
|
||||
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
||||
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
||||
>Download Selected</button>
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
<header {{ attributes }} class="bg-cyan-950">
|
||||
<header {{ attributes }} class="bg-cyan-950 z-40">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="h-16 flex flex-row items-center justify-between">
|
||||
<h1 class="text-3xl font-extrabold text-orange-500">Torsearch</h1>
|
||||
<twig:SearchBar />
|
||||
<div class="md:flex md:items-center md:gap-12">
|
||||
<nav aria-label="Global" class="md:block">
|
||||
<ul class="flex items-center gap-6 text-sm">
|
||||
<li><twig:ux:icon name="fluent:alert-12-regular" width="30px" class="text-gray-950 bg-orange-500 rounded-full p-2"/></li>
|
||||
<ul class="flex items-center align-middle gap-6 text-sm">
|
||||
<li id="active_download_notis" class="">
|
||||
<ul>
|
||||
{% if this.activeDownloads|length > 0 %}
|
||||
<li class="flex flex-col gap-1 align-middle">
|
||||
<twig:ux:icon name="flowbite:download-outline" width="1.75rem" color="#f97316" class="border-2 border-orange-500 rounded-md p-1" />
|
||||
{# {% include 'partial/alert-status.html.twig' %}#}
|
||||
{# <div class="absolute" style="top: 3.5rem; right: 7rem; z-index: 1000;">#}
|
||||
{# <div class="flex flex-col gap-1 bg-cyan-950 border-2 border-orange-500 text-white rounded-md p-3">#}
|
||||
{# <h3>Inception</h3>#}
|
||||
{# <div class="border-2 border-green-700 rounded-md w-full h-6 align-middle overflow-hidden">#}
|
||||
{# <div class="rounded-sm text-bold text-center text-white bg-green-600 h-5" style="width:47%">47%</div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li><twig:ux:icon name="fluent:alert-12-regular" width="2.5rem" class="text-orange-500 rounded-full p-2"/></li>
|
||||
<li>
|
||||
<a href="{{ path('app_logout') }}">
|
||||
<twig:ux:icon name="material-symbols:logout" width="25px" class="text-orange-500" />
|
||||
<twig:ux:icon name="material-symbols:logout" width="2rem" class="text-orange-500" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -17,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div {{ turbo_stream_listen('alerts') }} class="absolute top-10 right-10 size-96">
|
||||
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="absolute top-10 right-10">
|
||||
<div >
|
||||
<ul id="alert_list">
|
||||
</ul>
|
||||
|
||||
79
templates/components/MonitorList.html.twig
Normal file
79
templates/components/MonitorList.html.twig
Normal file
@@ -0,0 +1,79 @@
|
||||
<div{{ attributes }} class="min-w-48">
|
||||
<p class="text-white mb-1">The items you're currently monitoring to automatically download.</p>
|
||||
<table id="downloads" class="divide-y divide-gray-200 bg-gray-50 overflow-hidden rounded-lg table-fixed" {{ turbo_stream_listen('App\\Download\\Framework\\Entity\\Download') }}>
|
||||
<thead>
|
||||
<tr class="bg-orange-500 bg-filter bg-blur-lg bg-opacity-80 text-gray-950">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase min-w-[45ch] max-w-[45ch] truncate">
|
||||
Title
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Search Count
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Created at
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Last Search Date
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
{% if this.userMonitors.items|length > 0 %}
|
||||
{% for monitor in this.userMonitors.items %}
|
||||
<tr id="monitor_{{ monitor.id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-stone-800 min-w-[50ch] max-w-[50ch] truncate">
|
||||
{{ monitor.title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.searchCount }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.createdAt|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{{ monitor.lastSearch|date('m/d/Y h:i a') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||
{% if monitor.status == "New" %}
|
||||
<twig:StatusBadge color="orange" status="{{ monitor.status }}" />
|
||||
{% elseif monitor.status == "In Progress" or monitor.status == "Active" %}
|
||||
<twig:StatusBadge color="purple" status="{{ monitor.status }}" />
|
||||
{% else %}
|
||||
<twig:StatusBadge color="green" status="{{ monitor.status }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if this.userMonitors.items|length > 5 %}
|
||||
<tr id="monitor_view_all">
|
||||
<td colspan="5" class="py-2 whitespace-nowrap bg-orange-500 uppercase text-sm font-medium text-center text-white min-w-[50ch] max-w-[50ch] truncate">
|
||||
<a href="#">View All Monitors</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs uppercase text-center col-span-2 font-medium text-stone-800" colspan="2">
|
||||
No monitors
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if this.isWidget == false %}
|
||||
{% if this.userMonitors.items|length > 0 %}
|
||||
{% set paginator = this.userMonitors %}
|
||||
{% include 'partial/paginator.html.twig' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,67 +1,28 @@
|
||||
<nav {{ attributes }} class="flex h-screen flex-col justify-between bg-cyan-950">
|
||||
<div class="px-4 py-6">
|
||||
<ul class="mt-6 space-y-1">
|
||||
<nav {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950">
|
||||
<div class="px-4 py-4 flex flex-col gap-12">
|
||||
<h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1>
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<a
|
||||
href="{{ path('app_index') }}"
|
||||
class="block rounded-lg
|
||||
<a href="{{ path('app_index') }}"
|
||||
class="block rounded-lg
|
||||
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
||||
px-4 py-2 text-sm font-medium text-gray-50"
|
||||
>
|
||||
px-4 py-2 text-sm font-medium text-gray-50">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<details class="group [&_summary::-webkit-details-marker]:hidden">
|
||||
<summary
|
||||
class="flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 text-gray-50 hover:bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60"
|
||||
>
|
||||
<span class="text-sm font-medium"> Downloads </span>
|
||||
|
||||
<span class="shrink-0 transition duration-300 group-open:-rotate-180">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<ul class="mt-2 space-y-1 px-4">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700"
|
||||
>
|
||||
In Progress
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700"
|
||||
>
|
||||
Complete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="{{ path('app_downloads') }}"
|
||||
class="block rounded-lg
|
||||
bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60
|
||||
px-4 py-2 text-sm font-medium text-gray-50">
|
||||
Downloads
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="{{ path('app_media_preferences') }}"
|
||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700"
|
||||
>
|
||||
<a href="{{ path('app_media_preferences') }}"
|
||||
class="block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-gray-100 hover:text-stone-700">
|
||||
Preferences
|
||||
</a>
|
||||
</li>
|
||||
@@ -69,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sticky inset-x-0 bottom-0 border-t border-orange-500">
|
||||
<a href="#" class="flex items-center gap-2 p-4 bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60">
|
||||
<a href="#" class="nav-foot flex items-center gap-2 p-4 bg-orange-500 hover:bg-opacity-80 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60">
|
||||
<span class="rounded-full p-2 border-orange-500 border-2">
|
||||
<twig:ux:icon name="ri:user-line" width="30" class="text-gray-50"/>
|
||||
</span>
|
||||
|
||||
@@ -11,7 +11,15 @@
|
||||
placeholder="TV Show, Movie..."
|
||||
/>
|
||||
<button
|
||||
class="absolute top-1 right-1 flex items-center rounded bg-green-600 py-1 px-2.5 border border-transparent text-center text-sm text-white transition-all shadow-sm hover:shadow focus:bg-green-700 focus:shadow-none active:bg-green-700 hover:bg-green-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||
class="absolute top-1 right-1 flex items-center rounded
|
||||
bg-green-600 py-1 px-2.5 border border-transparent text-center
|
||||
text-sm text-white transition-all
|
||||
focus:bg-green-700 active:bg-green-700 hover:bg-green-700
|
||||
|
||||
text-white bg-green-600 text-sm
|
||||
border border-green-500
|
||||
backdrop-filter backdrop-blur-md bg-opacity-80
|
||||
"
|
||||
type="submit"
|
||||
>
|
||||
Search
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<div{{ attributes }}>
|
||||
<div class="p-4 flex flex-row gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||
<img class="w-24" src="{{ poster }}" />
|
||||
{% if poster != null and poster != "https://image.tmdb.org/t/p/w500" %}
|
||||
<img class="w-24 rounded-lg" src="{{ poster }}" />
|
||||
{% else %}
|
||||
<div class="w-32 h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||
<twig:ux:icon width="16" name="hugeicons:loading-01" />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="w-full flex flex-col">
|
||||
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
|
||||
{{ title }} - {{ year }}
|
||||
|
||||
3
templates/components/StatusBadge.html.twig
Normal file
3
templates/components/StatusBadge.html.twig
Normal file
@@ -0,0 +1,3 @@
|
||||
<span {{ attributes }} class="py-[3px] px-[7px] bg-{{ color|default('green') }}-600 rounded-lg inline-block text-center text-xs text-white">
|
||||
{{ status }}
|
||||
</span>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user