Compare commits

..

1 Commits

Author SHA1 Message Date
db19d28ae0 wip: base stream class 2025-05-23 08:24:10 -05:00
321 changed files with 3201 additions and 15292 deletions

View File

@@ -1,15 +0,0 @@
.git
.idea
.phpunit.cache
.php-cs-fixer.cache
.env.test
.gitignore
bolt.db
bash
build.xml
deploy.compose.yml
phpstan.dist.neon
phpunit.dist.xml
nomad.deploy.hcl
deployment.properties
var/download/*

21
.env
View File

@@ -39,22 +39,5 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
REDIS_HOST=redis://redis
###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###
AUTH_METHOD=form_login
###> drenso/symfony-oidc-bundle ###
OIDC_WELL_KNOWN_URL="https://oidc/.well-known"
OIDC_CLIENT_ID="Enter your OIDC client id"
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
OIDC_BYPASS_FORM_LOGIN=false
###< drenso/symfony-oidc-bundle ###
###> symfony/ntfy-notifier ###
# NTFY_DSN=ntfy://default/TOPIC
###< symfony/ntfy-notifier ###
NOTIFICATION_TRANSPORT=
NTFY_DSN=
MOVIES_PATH=/var/download/movies
TVSHOWS_PATH=/var/download/tvshows

View File

@@ -1,3 +0,0 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'

7
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.idea
bolt.db
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
@@ -18,9 +17,3 @@ bolt.db
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
.php-cs-fixer.cache
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###

View File

@@ -1,10 +1,19 @@
FROM code.caldwell.digital/home/torsearch-base:php8.4
FROM php:8.4-fpm-alpine3.21
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ENV APP_VERSION="0.0.0-dev"
RUN docker-php-ext-install pdo_mysql
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
# SETUP PHP-FPM CONFIG SETTINGS (max_children / max_requests)
RUN echo 'pm = dynamic' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
echo 'pm.max_children = 75' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
echo 'pm.start_servers = 30' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
echo 'pm.min_spare_servers = 5' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
echo 'pm.max_spare_servers = 30' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
echo 'pm.process_idle_timeout = 10s' >> /usr/local/etc/php-fpm.d/zz-docker.conf
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile
COPY --chmod=0775 ./bash/entrypoint.sh /usr/local/bin/
HEALTHCHECK --interval=5s --timeout=5s --retries=5 CMD [ "php", "/var/www/bin/console", "startup:status" ]
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh" ]
WORKDIR /var/www

11
Dockerfile.prod Normal file
View File

@@ -0,0 +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/nginx.conf /etc/apache2/sites-enabled/vhost.conf
RUN rm /etc/apache2/sites-enabled/000-default.conf

View File

@@ -6,7 +6,6 @@ import './bootstrap.js';
* which should already be in your base.html.twig.
*/
import './styles/app.css';
import PullToRefresh from 'pulltorefreshjs';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
@@ -18,3 +17,4 @@ var observer = new MutationObserver(function(mutations) {
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});

23
assets/bootstrap.js vendored
View File

@@ -1,26 +1,5 @@
import '@ungap/custom-elements'
import PreviewContentDialog from "./components/preview-content-dialog.js";
import EpisodeContainer from './components/episode-container.js';
import DownloadOptionTr from './components/download-option-tr.js';
import DownloadListRow from './components/download-list-row.js';
import MonitorListRow from './components/monitor-list-row.js';
import MovieContainer from "./components/movie-container.js";
import { startStimulusApp } from '@symfony/stimulus-bundle';
import Popover from '@stimulus-components/popover';
import Dialog from '@stimulus-components/dialog';
import Dropdown from '@stimulus-components/dropdown';
import 'animate.css';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
app.register('popover', Popover);
app.register('dialog', Dialog);
app.register('dropdown', Dropdown);
customElements.define('preview-content-dialog', PreviewContentDialog, {extends: 'dialog'});
customElements.define('episode-container', EpisodeContainer);
customElements.define('movie-container', MovieContainer);
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'});
// app.register('some_controller_name', SomeImportedController);

View File

@@ -1,111 +0,0 @@
export default class DownloadListRow extends HTMLTableRowElement {
constructor() {
super();
this.downloadId = this.getAttribute('download-id');
this.imdbId = this.getAttribute('imdb-id');
this.mediaTitle = this.getAttribute('media-title');
this.url = this.getAttribute('url');
this.filename = this.getAttribute('filename');
this.status = this.getAttribute('status');
this.progress = this.getAttribute('progress');
this.mediaType = this.getAttribute('media-type');
this.episodeId = this.getAttribute('episode-id');
this.createdAt = this.getAttribute('created-at');
this.updatedAt = this.getAttribute('updated-at');
// this.previewContent = this.previewContent.bind(this);
}
static get observedAttributes() {
return ['download-id', 'imdb-id', 'media-title', 'url', 'filename', 'status', 'progress', 'media-type', 'episode-id', 'created-at', 'updated-at'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">IMDB ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Title</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">URL</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Filename</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Status</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Progress</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Media Type</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Created At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Updated At</div>
</th>
</tr>
</thead>
<tbody>
<tr class="flex flex-col">
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('download-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('imdb-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-title') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('url') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('filename') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('status') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('progress') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-type') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('created-at') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('updated-at') ?? "-"}</div>
</td>
</tr>
</tbody>
</table>
`;
}
}

View File

@@ -1,187 +0,0 @@
export default class DownloadOptionTr extends HTMLTableRowElement {
H264_CODECS = {
'h264': 'h264',
'h.264': 'h264',
'x264': 'h264',
}
H265_CODECS = {
'h265': 'h265',
'h.265': 'h265',
'x265': 'h265',
'hevc': 'h265',
}
#downloadBtnEl;
#selectEpisodeInputEl;
url;
size;
quality;
resolution;
codec;
seeders;
provider;
languages;
mediaType;
season;
episode;
filename;
imdbId;
episodeId;
mediaTitle;
constructor() {
super();
this.url = this.getAttribute('url');
this.size = this.getAttribute('size');
this.quality = this.getAttribute('quality');
this.resolution = this.getAttribute('resolution');
this.codec = this.getAttribute('codec');
this.seeders = this.getAttribute('seeders');
this.provider = this.getAttribute('provider');
this.filename = this.getAttribute('filename');
this.imdbId = this.getAttribute('imdb-id');
this.languages = JSON.parse(this.getAttribute('languages'));
this.mediaType = this.getAttribute('media-type');
this.mediaTitle = this.getAttribute('media-title');
this.season = this.getAttribute('season') ?? null;
this.episode = this.getAttribute('episode') ?? null;
this.episodeId = this.getAttribute('episode-id') ?? null;
this.#downloadBtnEl = this.querySelector('.download-btn');
this.#selectEpisodeInputEl = this.querySelector('input[type="checkbox"]');
this.#downloadBtnEl.addEventListener('click', () => this.download());
}
get isSelected() {
return this.#selectEpisodeInputEl.checked;
}
set isSelected(value) {
this.#selectEpisodeInputEl.checked = value;
}
filter({ detail: { activeFilter } }) {
const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`)
let include = true;
this.classList.add('r-tablerow');
this.classList.remove('hidden');
optionHeader.classList.add('r-tablerow');
optionHeader.classList.remove('hidden');
this.querySelector('input[type="checkbox"]').checked = false;
if (!this.#validateResolutions(activeFilter.resolution)) {
include = false;
}
if (!this.#validateCodecs(activeFilter.codec)) {
include = false;
}
if (!this.#validateLanguages(activeFilter.language)) {
include = false;
}
if (!this.#validateQualities(activeFilter.quality)) {
include = false;
}
if (!this.#validateProviders(activeFilter.provider)) {
include = false;
}
if (false === include) {
this.classList.remove('r-tablerow');
this.classList.add('hidden');
optionHeader.classList.remove('r-tablerow');
optionHeader.classList.add('hidden');
}
return include;
}
download() {
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
url: this.url,
title: this.mediaTitle,
filename: this.filename,
mediaType: this.mediaType,
imdbId: this.imdbId,
episodeId: this.episodeId
})
})
.then(res => res.json())
.then(json => {
console.log(json)
})
}
#validateResolutions(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.resolution.trim().split(','));
}
#validateCodecs(selectedOptions) {
if (this.#validateIntersection(selectedOptions, Object.keys(this.H264_CODECS))) {
return this.#validateIntersection(
selectedOptions,
[...this.codec.trim().split(','), '', 'n/a']
);
}
if (this.#validateIntersection(selectedOptions, Object.keys(this.H265_CODECS))) {
return this.#validateIntersection(
selectedOptions,
[...this.codec.trim().split(','), '', 'n/a']
);
}
return false;
}
#validateQualities(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.quality.trim().split(','));
}
#validateProviders(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.provider.trim().split(','));
}
#validateLanguages(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.languages);
}
#validateIntersection(selectedOptions, localOptions) {
if (selectedOptions === null || selectedOptions === undefined) {
return true;
}
if (typeof selectedOptions === 'string' || selectedOptions instanceof String) {
selectedOptions = [selectedOptions];
}
if (selectedOptions.length === 0 ||
(selectedOptions.length === 1 && selectedOptions[0] === "") ||
(selectedOptions.length === 1 && selectedOptions[0] === "n/a")
) {
return true;
}
return this.#doesIntersect(localOptions, selectedOptions);
}
#doesIntersect(a, b) {
if (a.length === 0 || b.length === 0) {
return false;
}
return this.#intersect(a, b).length > 0;
}
#intersect(a, b) {
return a.filter(Set.prototype.has, new Set(b));
}
}

View File

@@ -1,76 +0,0 @@
export default class EpisodeContainer extends HTMLElement {
options = [];
showTitle;
#episodeSelectorEl;
#resultsToggleBtnEl;
#resultsTableEl;
#resultsCountBadgeEl;
#resultsCountNumberEl;
constructor() {
super();
this.showTitle = this.getAttribute('show-title');
this.#resultsTableEl = this.querySelector('.results-container');
this.#resultsToggleBtnEl = this.querySelector('.dropdown-button');
this.#resultsCountBadgeEl = this.querySelector('.results-count-badge');
this.#resultsCountNumberEl = this.querySelector('.results-count-number');
this.#episodeSelectorEl = this.querySelector('.episode-selector');
this.#resultsToggleBtnEl.addEventListener('click', () => this.toggleResults());
this.#resultsCountBadgeEl.addEventListener('click', () => this.toggleResults());
document.addEventListener('filterDownloadOptions', this.filter.bind(this));
document.addEventListener('downloadSelectedEpisodes', this.downloadSelectedResults.bind(this));
document.addEventListener('selectEpisodeForDownload', (e) => this.selectEpisodeForDownload(e.detail.select));
}
toggleResults() {
this.#resultsToggleBtnEl.classList.toggle('rotate-90');
this.#resultsToggleBtnEl.classList.toggle('-rotate-90');
this.#resultsTableEl.classList.toggle('hidden');
}
selectEpisodeForDownload(select) {
if (this.#episodeSelectorEl.disabled === false) {
this.#episodeSelectorEl.checked = select;
}
}
downloadSelectedResults() {
if (this.#episodeSelectorEl.disabled === false &&
this.#episodeSelectorEl.checked === true
) {
console.log('episode is selected')
this.options.forEach(option => {
if (option.isSelected === true) {
option.download();
}
option.isSelected = false;
})
}
}
filter({ detail: { activeFilter } }) {
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
this.options.forEach((option) => {
const include = option.filter({ detail: { activeFilter: activeFilter } });
if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
});
this.#resultsCountNumberEl.innerText = count;
}
}

View File

@@ -1,115 +0,0 @@
export default class MonitorListRow extends HTMLTableRowElement {
constructor() {
super();
this.downloadId = this.getAttribute('monitor-id');
this.imdbId = this.getAttribute('imdb-id');
this.mediaTitle = this.getAttribute('media-title');
this.url = this.getAttribute('url');
this.filename = this.getAttribute('filename');
this.status = this.getAttribute('status');
this.progress = this.getAttribute('progress');
this.mediaType = this.getAttribute('media-type');
this.episodeId = this.getAttribute('episode-id');
this.createdAt = this.getAttribute('created-at');
this.updatedAt = this.getAttribute('updated-at');
}
static get observedAttributes() {
return ['download-id', 'imdb-id', 'media-title', 'url', 'filename', 'status', 'progress', 'media-type', 'episode-id', 'created-at', 'updated-at'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">IMDB ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Title</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Season</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Status</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Search Count</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Media Type</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Created At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Updated At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
</th>
</tr>
</thead>
<tbody>
<tr class="flex flex-col">
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('monitor-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('imdb-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-title') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('season') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('status') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('search-count') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-type') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('created-at') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('last-search') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
</td>
</tr>
</tbody>
</table>
`;
}
}

View File

@@ -1,36 +0,0 @@
export default class MovieContainer extends HTMLElement {
#resultsTableEl;
#resultsCountNumberEl;
constructor() {
super();
this.#resultsTableEl = this.querySelector('.results-container');
this.#resultsCountNumberEl = document.querySelector('.results-count-number');
document.addEventListener('filterDownloadOptions', this.filter.bind(this));
}
filter({ detail: { activeFilter } }) {
const options = this.querySelectorAll('tr.download-option');
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
options.forEach((option) => {
const include = option.filter({ detail: { activeFilter: activeFilter } });
if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
});
this.#resultsCountNumberEl.innerText = count;
}
}

View File

@@ -1,35 +0,0 @@
export default class PreviewContentDialog extends HTMLDialogElement {
#headingEl;
#contentEl;
#closeBtnEl;
constructor() {
super();
this.#headingEl = this.querySelector('.modal-heading');
this.#contentEl = this.querySelector('.modal-content');
this.#closeBtnEl = this.querySelector('.modal-close');
this.setHeading = this.setHeading.bind(this);
this.setContent = this.setContent.bind(this);
this.#closeBtnEl.addEventListener('click', () => this.close());
document.addEventListener('hidePreviewContentModal', () => this.close());
document.addEventListener('showPreviewContentModal', (event) => {
this.display(event.detail);
});
}
setHeading(heading) {
this.#headingEl.innerHTML = heading;
}
setContent(content) {
this.#contentEl.innerHTML = content;
}
display({ heading, content }) {
this.setHeading(heading);
this.setContent(content);
this.showModal();
}
}

View File

@@ -1,34 +1,5 @@
{
"controllers": {
"@spomky-labs/pwa-bundle": {
"connection-status": {
"enabled": true,
"fetch": "eager"
},
"backgroundsync-form": {
"enabled": true,
"fetch": "eager"
},
"sync-broadcast": {
"enabled": true,
"fetch": "eager"
},
"prefetch-on-demand": {
"enabled": true,
"fetch": "eager"
}
},
"@symfony/ux-autocomplete": {
"autocomplete": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"tom-select/dist/css/tom-select.default.css": true,
"tom-select/dist/css/tom-select.bootstrap4.css": false,
"tom-select/dist/css/tom-select.bootstrap5.css": false
}
}
},
"@symfony/ux-live-component": {
"live": {
"enabled": true,

View File

@@ -1,46 +0,0 @@
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 {
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)
}
default() {
console.log('Looks like you need to add an action to your action button...')
}
monitorDispatch() {
fetch('/api/monitor/dispatch')
}
}

View File

@@ -12,7 +12,6 @@ export default class extends Controller {
filename: String,
mediaType: String,
imdbId: String,
episodeId: String
}
download() {
@@ -28,7 +27,6 @@ export default class extends Controller {
filename: this.filenameValue,
mediaType: this.mediaTypeValue,
imdbId: this.imdbIdValue,
episodeId: this.episodeIdValue
})
})
.then(res => res.json())

View File

@@ -7,7 +7,7 @@ import { getComponent } from '@symfony/ux-live-component';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['download', 'deleteFileInput']
static targets = ['download']
async initialize() {
this.component = await getComponent(this.element);
@@ -20,62 +20,13 @@ export default class extends Controller {
// 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)
// this.element.addEventListener('click', (event) => {
// let previewContentModal = document.querySelector('#previewContentModal');
// // previewContentModal.setHeading(event.target.dataset['title']);
// // previewContentModal.setContent('<p>Testing this here thingy-ma-bob!</p>');
// // previewContentModal.showModal();
// let content, heading = ""
// if (event.target.tagName !== "TR") {
// content = event.target.parentElement.previewContent();
// heading = event.target.parentElement.mediaTitle;
// } else {
// content = event.target.previewContent();
// heading = event.target.mediaTitle;
// }
//
// document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
// })
}
downloadTargetConnected(target) {
let downloads = this.element.querySelectorAll('tbody tr');
downloads.forEach(download => {
download.addEventListener('click', (event) => {
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent;
heading = "Download # " + event.target.parentElement.downloadId + " - \"" + event.target.parentElement.mediaTitle + "\"";
} else {
content = event.target.previewContent;
heading = "Download # " + event.target.downloadId + " - \"" + event.target.mediaTitle + "\"";
}
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}
pauseDownload(data) {
fetch(`/api/download/${data.params.id}/pause`, {method: 'PATCH'})
.then(res => res.json())
.then(json => console.debug(json));
}
resumeDownload(data) {
fetch(`/api/download/${data.params.id}/resume`, {method: 'PATCH'})
.then(res => res.json())
.then(json => console.debug(json));
}
deleteDownload(data) {
const deleteFileInput = document.querySelector(`#delete_file_${data.params.id}`)
fetch(`/api/download/${data.params.id}?deleteFile=${deleteFileInput.checked}`, {method: 'DELETE'})
.then(res => res.json())
.then(json => console.debug(json));
if (downloads.length > 5) {
target.classList.add('hidden');
}
}
// Add custom controller actions here

View File

@@ -1,43 +0,0 @@
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 outlets = ['navbar']
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)
}
toggleMenu() {
this.navbarOutlet.toggle();
}
// 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)
}
}

View File

@@ -14,22 +14,7 @@ export default class extends Controller {
static targets = ['icon']
connect() {
this.element.hideIcon = this.hideIcon.bind(this);
this.element.showIcon = this.showIcon.bind(this);
this.element.toggleIcon = this.toggleIcon.bind(this);
this.element.isVisibile = this.isVisible.bind(this);
}
isVisible() {
return !this.iconTarget.classList.contains('hidden');
}
showIcon() {
this.iconTarget.classList.remove('hidden');
}
hideIcon() {
this.iconTarget.classList.add('hidden');
}
toggleIcon() {
@@ -41,8 +26,6 @@ export default class extends Controller {
if (this.countValue === this.totalValue) {
this.toggleIcon();
this.countValue = 0;
console.log('filtering')
document.getElementById('filter').filterResults();
}
}
}

View File

@@ -8,12 +8,39 @@ import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['button', 'options']
static outlets = ['result-filter', 'dialog']
static outlets = ['result-filter']
static values = {
tmdbId: String,
imdbId: String,
title: String,
season: Number,
}
initialize() {
// Called once when the controller is first instantiated (per element)
// Here you can initialize variables, create scoped callables for event
// listeners, instantiate external libraries, etc.
// this._fooBar = this.fooBar.bind(this)
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
toggle() {
@@ -26,11 +53,29 @@ export default class extends Controller {
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvshows',
season: this.seasonValue
});
if (this.hasDialogOutlet) {
this.dialogOutlet.close();
}
}
async monitorSeason() {
await this.makeMonitor({
tmdbId: this.tmdbIdValue,
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvseason',
season: this.resultFilterOutlet.activeFilter['season'],
});
}
async monitorEpisode() {
// ToDo: figure out how to set episode
await this.makeMonitor({
tmdbId: this.tmdbIdValue,
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvepisode',
season: this.resultFilterOutlet.activeFilter['season'],
episode: '',
});
}
async makeMonitor(body) {
@@ -42,6 +87,7 @@ export default class extends Controller {
},
body: JSON.stringify(body)
});
return await response.json();
}
}

View 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)
})
}
}

View File

@@ -1,37 +0,0 @@
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 targets = ['monitorList']
monitorListTargetConnected(target) {
let monitors = this.element.querySelectorAll('tbody tr');
monitors.forEach(monitor => {
monitor.addEventListener('click', (event) => {
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent;
heading = "Monitor for \"" + event.target.parentElement.mediaTitle+ "\"";
} else {
content = event.target.previewContent;
heading = "Monitor for \"" + event.target.mediaTitle + "\"";
}
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}
deleteMonitor(data) {
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
.then(res => res.json())
.then(json => console.debug(json));
}
}

View File

@@ -6,6 +6,9 @@ import { Controller } from '@hotwired/stimulus';
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
static values = {
title: String,
tmdbId: String,
@@ -16,17 +19,77 @@ export default class extends Controller {
options = []
optionsLoaded = false
resultCountEl = null
async connect() {
this.resultCountEl = document.querySelector('#movie_results_count');
await this.setOptions();
}
async listTargetConnected() {
this.optionsLoaded = true;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.resultCountEl.innerText = this.options.length;
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
async setOptions() {
if (false === this.optionsLoaded) {
this.optionsLoaded = true;
await fetch(`/torrentio/movies/${this.tmdbIdValue}/${this.imdbIdValue}`)
.then(res => res.text())
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
});
}
}
// Keeps compatible with Filter & TV Shows
isActive() {
return true;
}
async filter(activeFilter) {
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
this.options.forEach((option) => {
const props = {
"resolution": option.querySelector('#resolution').textContent.trim(),
"codec": option.querySelector('#codec').textContent.trim(),
"provider": option.querySelector('#provider').textContent.trim(),
"languages": JSON.parse(option.dataset['languages']),
}
let include = true;
option.classList.remove('hidden');
option.querySelector('input[type="checkbox"]').checked = false;
for (let [key, value] of Object.entries(activeFilter)) {
if (value === "" || key === "season") {
continue;
}
if (key === "codec" && value === "h264") {
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "codec" && value === "h265") {
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "language") {
if (!props["languages"].includes(value)) {
include = false;
}
} else if (props[key] !== value) {
include = false;
}
}
if (false === include) {
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
});
}
}

View File

@@ -10,22 +10,16 @@ export default class extends Controller {
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() {
this.element.querySelectorAll('.nav-list a:not(.nav-foot)').forEach(link => {
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;
}
});
window.addEventListener("resize", (event) => {
});
}
toggle() {
this.element.parentElement.classList.toggle('hidden');
this.element.classList.toggle('animate__slideInLeft');
this.element.classList.toggle('fixed');
this.element.classList.toggle('z-20');
setActive() {
}
}

View File

@@ -6,116 +6,145 @@ import { Controller } from '@hotwired/stimulus';
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
languages = []
providers = []
qualities = []
seasons = []
activeFilter = {
"resolution": [],
"codec": [],
"language": [],
"provider": [],
"quality": [],
"resolution": "",
"codec": "",
"language": "",
"provider": "",
}
defaultOptions = '<option value="-">-</option>';
static outlets = ['tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected', 'currentSeason']
static outlets = ['movie-results', 'tv-results']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'selectAll', 'downloadSelected']
static values = {
'imdbId': String,
'media-type': String,
'episodes': Array,
'reverseMappedQualities': Object,
}
async connect() {
await this.setInitialFilter();
this.element.filterResults = this.filter.bind(this);
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
}
async setInitialFilter() {
const response = await fetch('/api/user/filters');
const filters = await response.json();
if (filters.length > 0) {
this.activeFilter = filters[0];
}
if (this.mediaTypeValue === "tvshows") {
this.activeFilter['season'] = 1;
}
await this.filter();
}
// Event is fired from movies/tvshows controllers to populate this data
async loadOptions({detail: { options }}) {
await options.forEach((option) => {
option.filter({detail: {activeFilter: this.activeFilter }});
async movieResultsOutletConnected(outlet) {
await this.parseDownloadOptionForFilter(outlet)
}
async tvResultsOutletConnected(outlet) {
await this.parseDownloadOptionForFilter(outlet)
}
async parseDownloadOptionForFilter(outlet) {
if (outlet.options.length === 0) {
await outlet.setOptions();
}
outlet.options.forEach((option) => {
this.addLanguages(option, option.dataset);
this.addProviders(option, option.dataset);
})
await this.filter();
}
selectAllEpisodes() {
document.dispatchEvent(new CustomEvent('selectEpisodeForDownload', {
detail: {
select: this.selectAllTarget.checked,
addLanguages(option, props) {
const languages = Object.assign([], JSON.parse(props['languages']));
languages.forEach((language) => {
if (!this.languages.includes(language)) {
this.languages.push(language);
}
}));
}
});
downloadSelectedEpisodes() {
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
}
filter() {
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
this.activeFilter = {
"resolution": this.#fetchValuesFromNodeList(this.resolutionTarget.selectedOptions),
"codec": this.#fetchValuesFromNodeList(this.codecTarget.selectedOptions),
"language": this.#fetchValuesFromNodeList(this.languageTarget.selectedOptions),
"provider": this.#fetchValuesFromNodeList(this.providerTarget.selectedOptions),
"quality": this.#fetchValuesFromNodeList(this.qualityTarget.selectedOptions),
const preferred = this.languageTarget.dataset.preferred;
if (preferred) {
this.languageTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
this.languageTarget.innerHTML += '<option value="">n/a</option>';
} else {
this.languageTarget.innerHTML = '<option value="">n/a</option>';
}
if ("tvshows" === this.mediaTypeValue) {
downloadSeasonSpan.innerText = this.seasonTarget.value;
this.activeFilter.season = this.seasonTarget.value;
}
const event = new CustomEvent('filterDownloadOptions', {
detail: {
activeFilter: this.activeFilter
}
})
// Event is picked up by the episode-container
// or movie-container web components
document.dispatchEvent(event);
}
setSeason(event) {
console.log('hurrrr');
this.tvEpisodeListOutlet.setSeason(event.target.value);
this.currentSeasonTarget.innerText = event.target.value;
}
downloadSeason() {
fetch(`/api/download/season/${this.imdbIdValue}/${this.activeFilter['season']}`, {
headers: {
'Content-Type': 'application/json'
}
})
}
#fetchValuesFromNodeList(nodeList) {
return [...nodeList].map(option => option.value)
}
#serializeSelectOptions(options) {
return this.defaultOptions + options.sort()
.map((option) => {
return '<option value="' + option + '">' + option + '</option>'
this.languageTarget.innerHTML += this.languages.sort()
.map((language) => {
const preferred = this.languageTarget.dataset.preferred;
if (preferred === language) {
return;
}
return '<option value="'+language+'">'+language+'</option>';
})
.join();
}
addProviders(option, props) {
if (!this.providers.includes(props['provider'])) {
this.providers.push(props['provider']);
}
const preferred = this.providerTarget.dataset.preferred;
if (preferred) {
this.providerTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
this.providerTarget.innerHTML += '<option value="">n/a</option>';
} else {
this.providerTarget.innerHTML = '<option value="">n/a</option>';
}
this.providerTarget.innerHTML += this.providers.sort()
.map((provider) => {
const preferred = this.languageTarget.dataset.preferred;
if (preferred === provider) {
return;
}
return '<option value="' + provider + '">' + provider + '</option>'
})
.join();
}
async filter() {
const currentSeason = this.activeFilter['season'];
let results = [];
this.activeFilter = {
"resolution": this.resolutionTarget.value,
"codec": this.codecTarget.value,
"language": this.languageTarget.value,
"provider": this.providerTarget.value,
}
if ("movies" === this.mediaTypeValue) {
results = this.movieResultsOutlets;
await results.forEach((list) => list.filter(this.activeFilter));
} else if ("tvshows" === this.mediaTypeValue) {
results = this.tvResultsOutlets;
this.activeFilter.season = this.seasonTarget.value;
await results.forEach((list) => list.filter(this.activeFilter, currentSeason, this.seasonTarget.value));
}
}
uncheckSelectAllBtn() {
this.selectAllTarget.checked = false;
}
selectAllEpisodes() {
this.tvResultsOutlets.forEach((episode) => {
if (episode.isActive()) {
episode.selectEpisodeForDownload()
}
});
}
downloadSelectedEpisodes() {
this.tvResultsOutlets.forEach(episode => {
if (episode.isActive() && episode.isSelected()) {
episode.download();
}
});
this.selectAllTarget.checked = false;
}
}

View File

@@ -1,64 +0,0 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
initialize() {
this._onPreConnect = this._onPreConnect.bind(this);
this._onConnect = this._onConnect.bind(this);
}
connect() {
document.querySelector("#search").onsubmit = (event) => {
event.preventDefault();
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
window.location.href = `/search?term=${autocompleteController.tomSelect.lastValue}`
}
document.querySelector("#search-button").addEventListener('click', (event) => {
event.preventDefault();
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
window.location.href = `/search?term=${autocompleteController.tomSelect.lastQuery}`
});
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect);
this.element.addEventListener('autocomplete:connect', this._onConnect);
}
disconnect() {
// You should always remove listeners when the controller is disconnected to avoid side-effects
this.element.removeEventListener('autocomplete:connect', this._onConnect);
this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect);
}
_onPreConnect(event) {
// TomSelect has not been initialized - options can be changed
// console.log(event.detail); // Options that will be used to initialize TomSelect
event.detail.options.onItemAdd = (value, $item) => {
const params = value.split('|')
window.location.href = `/result/${params[0]}/${params[1]}`
};
event.detail.options.render.loading = (data, escape) => {
return `
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
</span>`;
}
event.detail.options.render.option = (data, escape) => {
console.log(data);
if (data.data.overview.length > 60) {
data.data.overview = data.data.overview.substring(0, 107) + "...";
}
return `<div class="flex flex-row">
<img src="${data.data.poster}" class="w-16 rounded-md">
<div class="p-2 flex flex-col">
<h2>${data.data.title}</h2>
<p class="max-w-[60ch] text-wrap">${data.data.overview}</p>
</div>
</div>`;
}
}
_onConnect(event) {
// TomSelect has just been initialized and you can access details from the event
// console.log(event.detail.tomSelect); // TomSelect instance
// console.log(event.detail.options); // Options used to initialize TomSelect
}
}

View File

@@ -1,59 +0,0 @@
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 {
async initialize() {
this.component = await getComponent(this.element);
this.component.on('render:finished', (component) => {
console.log(component);
});
if (window.location.hash) {
let targetElement = document.querySelector(window.location.hash);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
targetElement.classList.add('animate__animated', 'animate__pulse', 'animate__faster');
}
}
}
setSeason(season) {
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
this.component.set('pageNumber', 1);
this.component.set('season', parseInt(season));
this.component.render();
}
paginate(event) {
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
this.component.set('episodeNumber', null);
this.component.action('paginate', {page: event.params.page});
this.component.render();
}
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)
}
}

View File

@@ -18,21 +18,151 @@ export default class extends Controller {
active: Boolean,
};
static targets = ['list', 'count', 'episodeSelector',]
static targets = ['list', 'count', 'episodeSelector']
static outlets = ['loading-icon']
options = []
optionsLoaded = false
listTargetConnected() {
this.element.options = this.element.querySelectorAll('tbody tr');
if (this.element.options.length > 0) {
this.element.options.forEach((option) =>
option.querySelector('.download-btn').dataset['title'] = this.titleValue
);
this.element.options[0].querySelector('input[type="checkbox"]').checked = true;
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}}));
} else {
this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true;
async connect() {
await this.setOptions();
}
async setOptions() {
if (true === this.activeValue && this.optionsLoaded === false) {
this.optionsLoaded = true;
await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
.then(res => res.text())
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
if (this.options.length > 0) {
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.options[0].querySelector('input[type="checkbox"]').checked = true;
} else {
this.episodeSelectorTarget.disabled = true;
}
this.loadingIconOutlet.increaseCount();
});
}
}
//
// async clearCache() {
// await fetch(`/torrentio/tvshows/clear/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
// .then(res => res.text())
// .then(response => {});
// }
async setActive() {
this.activeValue = true;
this.element.classList.remove('hidden');
if (false === this.optionsLoaded) {
await this.setOptions();
}
}
async setInActive() {
this.activeValue = false;
// if (true === this.hasEpisodeSelectorTarget()) {
this.episodeSelectorTarget.checked = false;
// }
this.element.classList.add('hidden');
}
isActive() {
return this.activeValue;
}
isSelected() {
return this.episodeSelectorTarget.checked;
}
selectEpisodeForDownload() {
if (true === this.isActive() && this.episodeSelectorTarget.disabled === false) {
this.episodeSelectorTarget.checked = !this.episodeSelectorTarget.checked;
}
}
toggleList() {
this.listTarget.classList.toggle('hidden');
}
download() {
this.options.forEach(option => {
const optionSelector = option.querySelector('input[type="checkbox"]');
if (true === optionSelector.checked) {
const downloadBtn = option.querySelector('button.download-btn');
const downloadBtnController = this.application.getControllerForElementAndIdentifier(downloadBtn, 'download-button');
downloadBtnController.download();
optionSelector.checked = false;
this.episodeSelectorTarget.checked = false;
}
})
}
async filter(activeFilter, currentSeason, newSeason) {
if (currentSeason !== activeFilter['season']) {
if (this.seasonValue === newSeason) {
await this.setActive();
} else {
await this.setInActive();
}
}
if (false === this.isActive()) {
return;
}
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
this.options.forEach((option) => {
const props = {
"resolution": option.querySelector('#resolution').textContent.trim(),
"codec": option.querySelector('#codec').textContent.trim(),
"provider": option.querySelector('#provider').textContent.trim(),
"languages": JSON.parse(option.dataset['languages']),
}
let include = true;
option.classList.remove('hidden');
option.querySelector('input[type="checkbox"]').checked = false;
for (let [key, value] of Object.entries(activeFilter)) {
if (value === "" || key === "season") {
continue;
}
if (key === "codec" && value === "h264") {
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "codec" && value === "h265") {
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "language") {
if (!props["languages"].includes(value)) {
include = false;
}
} else if (props[key] !== value) {
include = false;
}
}
if (false === include) {
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
this.countTarget.innerText = count;
});
}
}

View File

@@ -1,57 +0,0 @@
import { Controller } from '@hotwired/stimulus';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
calendar = null;
initialize() {
}
connect() {
this.calendar = new Calendar(this.element, {
plugins: [ dayGridPlugin, timeGridPlugin ],
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
editable: true, // Allow events to be dragged and resized
events: '/api/events', // Symfony route to fetch events
eventDrop: function(info) {
// Handle event drop (e.g., update event in database via AJAX)
},
eventResize: function(info) {
// Handle event resize (e.g., update event in database via AJAX)
}
});
this.calendar.render();
// this.calendar = new Calendar(this.element, {
// plugins: [ dayGridPlugin, timeGridPlugin, listPlugin ],
// initialView: 'dayGridMonth',
// headerToolbar: {
// left: 'prev,next today',
// center: 'title',
// right: 'dayGridMonth,timeGridWeek,listWeek'
// }
// });
// this.calendar.render();
// calendar.render();
}
// 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)
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4c-4.41 0-8 3.59-8 8s3.59 8 8 8s8-3.59 8-8s-3.59-8-8-8m5 11.59L15.59 17L12 13.41L8.41 17L7 15.59L10.59 12L7 8.41L8.41 7L12 10.59L15.59 7L17 8.41L13.41 12z" opacity=".3"/><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10s10-4.47 10-10S17.53 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m3.59-13L12 10.59L8.41 7L7 8.41L10.59 12L7 15.59L8.41 17L12 13.41L15.59 17L17 15.59L13.41 12L17 8.41z"/></svg>

Before

Width:  |  Height:  |  Size: 526 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><mask id="ipTPauseOne0"><g fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="4"><path fill="#555" d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4S4 12.954 4 24s8.954 20 20 20Z"/><path stroke-linecap="round" d="M19 18v12m10-12v12"/></g></mask></defs><path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTPauseOne0)"/></svg>

Before

Width:  |  Height:  |  Size: 407 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><mask id="ipTPlay0"><g fill="#555" stroke="#fff" stroke-linejoin="round" stroke-width="4"><path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4S4 12.954 4 24s8.954 20 20 20Z"/><path d="M20 24v-6.928l6 3.464L32 24l-6 3.464l-6 3.464z"/></g></mask></defs><path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTPlay0)"/></svg>

Before

Width:  |  Height:  |  Size: 392 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0" stroke="currentColor" stroke-dasharray="64" stroke-dashoffset="64" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12c0 -4.97 4.03 -9 9 -9c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.15s" values="0;0.3"/><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0"/></path></svg>

Before

Width:  |  Height:  |  Size: 517 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 0 0"><path fill="currentColor" fill-rule="evenodd" d="M0 3.75A.75.75 0 0 1 .75 3h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 3.75M0 8a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8m.75 3.5a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="currentColor" stroke-width="1.5" d="M2 12c0-3.771 0-5.657 1.172-6.828S6.229 4 10 4h4c3.771 0 5.657 0 6.828 1.172S22 8.229 22 12v2c0 3.771 0 5.657-1.172 6.828S17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.172S2 17.771 2 14z"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M7 4V2.5M17 4V2.5M2.5 9h19"/><path fill="currentColor" d="M18 17a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 653 B

View File

@@ -1 +0,0 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="m0 11l2-2l5 5L18 3l2 2L7 18z"/></svg>

Before

Width:  |  Height:  |  Size: 127 B

View File

@@ -10,199 +10,4 @@
h2 {
font-size: var(--text-xl);
}
.rounded-ms {
border-radius: 0.275rem;
}
}
@layer components {
.alert {
@apply text-white text-sm min-w-[250px] border px-4 py-3 rounded-md
}
.alert-success {
@apply bg-green-950 hover:bg-green-900 border-green-500
}
.alert-warning {
@apply bg-yellow-500 hover:bg-yellow-600 border-yellow-400 text-black
}
}
:root {
--fc-border-color: #a65b27;
--fc-page-bg-color: #a65b27;
}
/* Prevent scrolling while dialog is open */
body:has(dialog[data-dialog-target="dialog"][open]) {
overflow: hidden;
}
/* Customize the dialog backdrop */
dialog {
box-shadow: 0 0 0 100vw rgb(0 0 0 / 0.5);
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
dialog[open] {
animation: fade-in 100ms ease-in forwards;
}
/* Add animations */
dialog[data-dialog-target="dialog"][open] {
animation: fade-in 200ms forwards;
}
dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards;
}
.text-input {
@apply bg-gray-50 text-gray-50 px-2 py-1 bg-transparent border-b-2 border-orange-400
}
.text-input[multiple="multiple"] {
@apply bg-transparent backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md
}
.text-input option[checked="checked"],
.text-input option[checked],
.text-input option[selected] {
@apply bg-orange-500/60
}
.submit-button {
@apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40
}
.r-tablecell {
display: none;
}
.r-tablerow {
display: flex;
}
@media screen and (min-width: 768px) {
.r-tablecell {
display: table-cell;
}
.r-tablerow {
display: table-row;
}
}
.options-table {
display: flex;
:last-child {
border-bottom: none;
}
}
@media screen and (min-width: 768px) {
.options-table {
display: inline-table;
}
}
#search .ts-wrapper.single .ts-control::after {
display: none !important;
}
#search .ts-control {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #fff !important;
padding-left: 0;
input {
color: #fff !important;
padding: 0;
}
}
#search .ts-dropdown {
background: unset;
@apply bg-orange-500/80 backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md z-20
}
#search .ts-dropdown .ts-dropdown-content .option.active {
background: unset;
@apply bg-orange-500/80 text-black font-bold rounded-md
}
.progress {
display: grid;
grid-template: 1fr / 1fr;
place-items: center;
}
.progress > * {
grid-column: 1 / 1;
grid-row: 1 / 1;
}
.progress .background {
z-index: 1;
place-self: start;
}
.progress .number {
z-index: 2;
background: transparent;
}
#filter {
.ts-wrapper {
box-shadow: none !important;
padding: 0;
.ts-control {
border: none !important;
box-shadow: none !important;
@apply bg-orange-500/60 backdrop-filter backdrop-blur-md;
}
.item[data-ts-item] {
background-image: none !important;
border: none;
box-shadow: none;
text-shadow: none;
@apply bg-orange-500 rounded-ms font-bold text-black;
}
@apply border border-orange-500 bg-transparent rounded-ms;
}
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {
@apply border-l border-l-orange-600 !important;
}
}
.filter-label {
@apply flex flex-col gap-1 justify-between;
}
/** FullCalendar **/
#upcoming_episodes_calendar .fc-event-main .fc-event-title-container {
cursor: pointer !important;
}
.fc-col-header-cell {
@apply bg-orange-500/60 text-white;
}

2
bash/app/wget_download.sh Executable file
View File

@@ -0,0 +1,2 @@
# $1 = movies/tvshows/etc, $2 = title of media, $3 = URL of download
cd /var/download/${1} && if [ ! -d "${2}" ]; then mkdir "${2}"; fi && cd "${2}" && wget "${3}"

View File

@@ -1,22 +0,0 @@
# torsearch-app is built from this base
export APP_FRANKENPHP_TAG=php8.4
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
docker build -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base:latest
# torsearch-worker & torsearch-scheduler are built from this base
export WORKER_FRANKENPHP_TAG=php8.4-alpine
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
docker build -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base-worker:latest
# torsearch-worker-supervisord
export ALPINE_VERSION=3.22
#docker buildx build --platform=linux/amd64,linux/arm64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" .
docker build -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker-supervisord:latest --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" .
docker push code.caldwell.digital/home/torsearch-base-worker-supervisord:latest

View File

@@ -1,11 +1,6 @@
{
log {
level DEBUG
}
}
dev.caldwell.digital:443
dev.caldwell.digital:443 {
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
reverse_proxy app:80
reverse_proxy app:80
}

13
bash/entrypoint.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Sleep for a second to ensure DB is awake and ready
SLEEP_TIME=$(shuf -i 2-5 -n 1)
echo "> Sleeping for ${SLEEP_TIME} seconds to wait for the database"
echo "> If there are errors after the migration runs, it's possible another container (scheduler, worker, etc.) already ran the migrations"
sleep $SLEEP_TIME
# Provision database
php /var/www/bin/console doctrine:migrations:migrate --no-interaction
php /var/www/bin/console db:seed
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

32
bash/nginx.conf Executable file
View File

@@ -0,0 +1,32 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
root /var/www/public;
location /hub/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://mercure/;
}
location / {
# try to serve file directly, fallback to index.php
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
}

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

View File

@@ -2,7 +2,6 @@ services:
caddy:
image: caddy:2.9.1
restart: unless-stopped
tty: true
cap_add:
- NET_ADMIN
ports:
@@ -13,59 +12,68 @@ services:
- $PWD/bash/caddy:/etc/caddy
- $PWD/bash/certs:/etc/ssl
app:
build: .
restart: unless-stopped
volumes:
- $PWD:/app
- $PWD/var/download:/var/download
- mercure_data:/data
- mercure_config:/config
tty: true
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on:
database:
condition: service_healthy
worker:
build:
dockerfile: docker/Dockerfile.worker
context: .
restart: unless-stopped
volumes:
- $PWD:/app
- $PWD/var/download:/var/download
tty: true
environment:
TZ: America/Chicago
scheduler:
build:
dockerfile: docker/Dockerfile.scheduler
context: .
restart: unless-stopped
volumes:
- $PWD:/app
- $PWD/var/download:/var/download
tty: true
environment:
TZ: America/Chicago
WORKER_MONITOR: 2
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
app:
build:
dockerfile: docker/Dockerfile.app
context: .
ports:
- "8001:80"
volumes:
- ./:/var/www
depends_on:
database:
condition: service_healthy
worker:
build:
dockerfile: docker/Dockerfile.worker
context: .
volumes:
- ./:/app
- ./var/downloads/movies:/var/download/movies
- ./var/downloads/tvshows:/var/download/tvshows
command: -vvv --time-limit=3600
env_file:
- .env
depends_on:
app:
condition: service_healthy
scheduler:
build:
dockerfile: docker/Dockerfile.scheduler
context: .
volumes:
- ./:/var/www
- ./var/download:/var/download
command: -vv --time-limit=3600
depends_on:
app:
condition: service_healthy
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3000:80"
environment:
TZ: America/Chicago
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database:
@@ -75,25 +83,19 @@ services:
volumes:
- mysql:/var/lib/mysql
environment:
TZ: America/Chicago
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
timeout: 10s
retries: 10
adminer:
image: adminer
ports:
- "8081:8080"
environment:
ADMINER_DEFAULT_SERVER: database
volumes:
mysql:

View File

@@ -4,70 +4,49 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"1tomany/rich-bundle": "^1.8",
"aimeos/map": "^3.12",
"chrisullyott/php-filesize": "^4.2",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-fixtures-bundle": "^4.1",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3",
"dragonmantank/cron-expression": "^3.4",
"drenso/symfony-oidc-bundle": "^4.2",
"guzzlehttp/guzzle": "^7.9",
"league/pipeline": "^1.1",
"nesbot/carbon": "^3.9",
"nihilarr/parse-torrent-name": "^0.0.1",
"nyholm/psr7": "*",
"p3k/emoji-detector": "^1.2",
"php-http/cache-plugin": "^2.0",
"php-tmdb/api": "^4.1",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1",
"predis/predis": "^2.4",
"runtime/frankenphp-symfony": "^0.2.0",
"spatie/icalendar-generator": "^3.0",
"spomky-labs/pwa-bundle": "^1.2",
"stof/doctrine-extensions-bundle": "^1.14",
"symfony/asset": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/filesystem": "7.3.*",
"symfony/finder": "7.3.*",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",
"symfony/doctrine-messenger": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/filesystem": "7.2.*",
"symfony/finder": "7.2.*",
"symfony/flex": "^2",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/ldap": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/ldap": "7.2.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "7.3.*",
"symfony/notifier": "7.3.*",
"symfony/ntfy-notifier": "7.3.*",
"symfony/object-mapper": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/scheduler": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/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.3.*",
"symfony/ux-autocomplete": "^2.27",
"symfony/twig-bundle": "7.2.*",
"symfony/ux-icons": "^2.24",
"symfony/ux-live-component": "^2.24",
"symfony/ux-turbo": "^2.24",
"symfony/ux-twig-component": "^2.24",
"symfony/yaml": "7.3.*",
"symfonycasts/reset-password-bundle": "^1.23",
"symfony/yaml": "7.2.*",
"symfonycasts/tailwind-bundle": "^0.10.0",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"web-token/jwt-library": "^4.0"
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
@@ -75,9 +54,6 @@
"symfony/flex": true,
"symfony/runtime": true
},
"platform": {
"php": "8.4"
},
"bump-after-update": true,
"sort-packages": true
},
@@ -113,7 +89,6 @@
"post-update-cmd": [
"@auto-scripts"
],
"tail": "docker compose exec app ./bin/console tailwind:build --watch",
"sym": "docker compose exec app ./bin/console"
},
"conflict": {
@@ -122,14 +97,13 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.3.*"
"require": "7.2.*"
}
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.3",
"symfony/maker-bundle": "^1.62",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*"
}
}

4938
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,4 @@ return [
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
];

View File

@@ -18,12 +18,6 @@ doctrine:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
# App:
# type: attribute
# is_bundle: false
# dir: '%kernel.project_dir%/src/Entity'
# prefix: 'App\Entity'
# alias: App
Download:
type: attribute
is_bundle: false
@@ -41,13 +35,7 @@ doctrine:
is_bundle: false
dir: '%kernel.project_dir%/src/Monitor/Framework/Entity'
prefix: 'App\Monitor\Framework\Entity'
alias: Monitor
EventLog:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/EventLog/Framework/Entity'
prefix: 'App\EventLog\Framework\Entity'
alias: EventLog
alias: Download
controller_resolver:
auto_mapping: false

View File

@@ -1,19 +0,0 @@
drenso_oidc:
#default_client: default # The default client, will be aliased to OidcClientInterface
clients:
default: # The client name, each client will be aliased to its name (for example, $defaultOidcClient)
# Required OIDC client configuration
well_known_url: '%env(OIDC_WELL_KNOWN_URL)%'
client_id: '%env(OIDC_CLIENT_ID)%'
client_secret: '%env(OIDC_CLIENT_SECRET)%'
redirect_route: '/login/oidc/auth'
# Extra configuration options
#redirect_route: '/login_check'
#custom_client_headers: []
# Add any extra client
#link: # Will be accessible using $linkOidcClient
#well_known_url: '%env(LINK_WELL_KNOWN_URL)%'
#client_id: '%env(LINK_CLIENT_ID)%'
#client_secret: '%env(LINK_CLIENT_SECRET)%'

View File

@@ -11,7 +11,7 @@ framework:
trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ]
session:
handler_id: '%env(REDIS_HOST)%'
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
#esi: true
#fragments: true

View File

@@ -1,7 +0,0 @@
framework:
mailer:
dsn: 'smtp://%env(SMTP_USER)%:%env(SMTP_PASS)%@%env(SMTP_HOST)%:%env(SMTP_PORT)%'
envelope:
sender: '%env(SMTP_FROM)%'
headers:
From: '%env(SMTP_FROM_NAME)% <%env(SMTP_FROM)%>'

View File

@@ -10,42 +10,10 @@ framework:
options:
use_notify: true
check_delayed_interval: 60000
queue_name: default
retry_strategy:
max_retries: 1
multiplier: 1
download:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
queue_name: download
retry_strategy:
max_retries: 3
multiplier: 1
monitor:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
queue_name: monitor
retry_strategy:
max_retries: 1
multiplier: 1
media_cache:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
queue_name: media_cache
retry_strategy:
max_retries: 1
multiplier: 1
failed: 'doctrine://default?queue_name=failed'
default_bus: messenger.bus.default
@@ -56,13 +24,12 @@ framework:
routing:
# Route your messages to the transports
# 'App\Message\YourMessage': async
'App\Download\Action\Command\DownloadMediaCommand': download
'App\Download\Action\Command\DownloadSeasonCommand': async
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': monitor
'App\Monitor\Action\Command\MonitorTvSeasonCommand': monitor
'App\Monitor\Action\Command\MonitorTvShowCommand': monitor
'App\Monitor\Action\Command\MonitorMovieCommand': monitor
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': media_cache
'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
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': async
# when@test:
# framework:

View File

@@ -1,13 +0,0 @@
framework:
notifier:
chatter_transports:
texter_transports:
ntfy: '%notification.ntfy.dsn%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -1,3 +0,0 @@
framework:
property_info:
with_constructor_extractor: true

View File

@@ -1,21 +0,0 @@
pwa:
manifest:
enabled: true
name: "Torsearch"
short_name: "torsearch"
start_url: "/"
display: "standalone"
id: "/"
background_color: "#f98e44"
theme_color: "#083344"
description: Torsearch provides a simple and intuitive way to manage your personal media library.
icons:
- src: "/icon.png"
sizes: [ 192 ]
- src: "/icon.png"
sizes: [ 192 ]
purpose: maskable
categories:
- entertainment
- multimedia
- utilities

View File

@@ -1,2 +0,0 @@
symfonycasts_reset_password:
request_password_repository: App\User\Framework\Repository\ResetPasswordRequestRepository

View File

@@ -10,9 +10,6 @@ security:
class: App\User\Framework\Entity\User
property: email
app_oidc:
id: App\User\Framework\Security\OidcUserProvider
app_ldap:
id: App\User\Framework\Security\LdapUserProvider
@@ -21,20 +18,14 @@ security:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
logout:
path: /logout
provider: app_oidc
lazy: true
provider: app_local
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
oidc:
login_path: '/login/oidc'
check_path: '/login/oidc/auth'
enable_end_session_listener: true
http_basic:
realm: Secured Area
entry_point: form_login
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@@ -45,8 +36,6 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }

View File

@@ -1,7 +0,0 @@
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
timestampable: true

View File

@@ -1,10 +1,5 @@
twig:
globals:
version: '%app.version%'
file_name_pattern: '*.twig'
date:
format: 'm/d/Y'
timezone: '%env(default:app.default.timezone:TZ)%'
when@test:
twig:

View File

@@ -1,31 +1,7 @@
controllersBase:
controllersIndex:
resource:
path: ../src/Base/Framework/Controller/
namespace: App\Base\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersEventLog:
resource:
path: ../src/EventLog/Framework/Controller/
namespace: App\EventLog\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersLibrary:
resource:
path: ../src/Library/Framework/Controller/
namespace: App\Library\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersSearch:
resource:
path: ../src/Search/Framework/Controller/
namespace: App\Search\Framework\Controller
path: ../src/Controller/
namespace: App\Controller
type: attribute
defaults:
schemes: [ 'https' ]
@@ -53,19 +29,3 @@ controllersMonitor:
type: attribute
defaults:
schemes: ['https']
controllersTorrentio:
resource:
path: ../src/Torrentio/Framework/Controller
namespace: App\Torrentio\Framework\Controller
type: attribute
defaults:
schemes: ['https']
controllersTmdb:
resource:
path: ../src/Tmdb/Framework/Controller
namespace: App\Tmdb\Framework\Controller
type: attribute
defaults:
schemes: ['https']

View File

@@ -1,3 +0,0 @@
ux_autocomplete:
resource: '@AutocompleteBundle/config/routes.php'
prefix: '/autocomplete'

61
config/security.yaml Normal file
View 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

View File

@@ -4,26 +4,15 @@
# 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:
# App
app.url: '%env(APP_URL)%'
app.version: '%env(default:app.default.version:APP_VERSION)%'
# Debrid Services
app.debrid.real_debrid.key: '%env(REAL_DEBRID_KEY)%'
# TMDB Key
app.meta_provider.tmdb.key: '%env(TMDB_API)%'
# Media
media.base_path: '/var/download'
media.default_movies_dir: movies
media.default_tvshows_dir: tvshows
media.movies_path: '/var/download/%env(default:media.default_movies_dir:MOVIES_PATH)%'
media.movies_path: '%env(default:media.default_movies_dir:MOVIES_PATH)%'
media.tvshows_path: '/var/download/%env(default:media.default_tvshows_dir:TVSHOWS_PATH)%'
# Mercure
app.mercure.url: 'http://app/.well-known/mercure'
app.mercure.public_url: '%env(APP_URL)%/.well-known/mercure'
app.mercure.url: 'http://mercure/.well-known/mercure'
app.mercure.public_url: '%env(APP_URL)%/hub/.well-known/mercure'
# Cache
app.cache.adapter: '%env(default:app.cache.adapter.default:CACHE_ADAPTER)%'
@@ -31,23 +20,6 @@ parameters:
app.cache.adapter.default: 'filesystem'
app.cache.redis.host.default: 'redis://redis'
# Various configs
app.default.version: '0.0.0-dev'
app.default.timezone: 'America/Chicago'
# Auth
auth.default.method: 'form_login'
auth.method: '%env(default:auth.default.method:AUTH_METHOD)%'
auth.oidc.well_known_url: '%env(OIDC_WELL_KNOWN_URL)%'
auth.oidc.client_id: '%env(OIDC_CLIENT_ID)%'
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
# Notifications
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
notification.ntfy.dsn: '%env(NTFY_DSN)%'
services:
# default configuration for services in *this* file
_defaults:

View File

@@ -1,39 +1,49 @@
services:
app:
image: registry.caldwell.digital/home/torsearch-app:${TAG}
web:
image: code.caldwell.digital/home/torsearch/web:latest
ports:
- "${SWARM_PORT}:80"
environment:
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
- '8001:80'
volumes:
- $PWD/bash/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
app:
condition: service_healthy
app:
image: code.caldwell.digital/home/torsearch/app:${TAG}
deploy:
replicas: 2
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
- mercure_data:/data
- mercure_config:/config
worker:
image: registry.caldwell.digital/home/torsearch-worker:${TAG}
image: code.caldwell.digital/home/torsearch/app:${TAG}
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
- /mnt/media/downloads:/var/download
command: php ./bin/console messenger:consume async -v --time-limit=3600 --limit=10
deploy:
replicas: 2
depends_on:
- app
scheduler:
image: registry.caldwell.digital/home/torsearch-scheduler:${TAG}
image: code.caldwell.digital/home/torsearch/app:${TAG}
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
depends_on:
- app
- /mnt/media/downloads:/var/download
command: php ./bin/console messenger:consume scheduler_monitor -vv --time-limit=3600
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3000:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
volumes:
mercure_config:

View File

@@ -10,7 +10,6 @@ templates
var
vendor
build.xml
.git
.env
.env.local
composer.json

View File

@@ -1,14 +1,19 @@
FROM code.caldwell.digital/home/torsearch-base:php8.4
FROM trafex/php-nginx:3.9.0
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
USER root
COPY . /app
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
RUN apk add --no-cache \
php84-pdo_mysql \
php84-simplexml
ENTRYPOINT [ "/usr/local/bin/docker-entrypoint" ]
USER nobody
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
COPY --chown=nobody:nobody . /var/www
COPY --chmod=0775 ./bash/entrypoint.sh /usr/local/bin/
COPY --chmod=0755 ./bash/nginx.conf /etc/nginx/conf.d/site.conf
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
HEALTHCHECK --interval=5s --timeout=5s --retries=5 CMD [ "php", "/var/www/bin/console", "startup:status" ]
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh" ]
WORKDIR /var/www

View File

@@ -1,14 +0,0 @@
ARG FRANKENPHP_TAG
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache

View File

@@ -1,36 +0,0 @@
#####
# This version of Torsearch runs the scheduler, downloader, and worker
# in a single container. Each process is managerd by supervisord
# and can be configured via environment variables and more
# than one of these containers cans till be run.
#####
ARG ALPINE_VERSION="3.22"
FROM alpine:${ALPINE_VERSION} AS build_stage
RUN apk add --no-cache \
curl \
php84 \
php84-curl \
php84-fileinfo \
php84-gd \
php84-mbstring \
php84-mysqli \
php84-opcache \
php84-openssl \
php84-pdo_mysql \
php84-xml \
supervisor
RUN ln -s /usr/bin/php84 /usr/bin/php
RUN mkdir -p /etc/supervisor/conf.d
# We start supervisord and supervisord starts
# respective service in the configuration file.
ENTRYPOINT ["/app/bin/console", "init:worker"]
# Message transports can be enabled by passing them as command options.
# Inlcluding a number with the option will start that amount of processes
# for the transport. Not supplying a number will default to 1.
# --download --monitor OR --download 2 --monitor

View File

@@ -1,21 +1,7 @@
###
# This version of Torsearch can run the scheduler, downloader, and
# worker in one container. Each process is managerd by supervisord
# and can be configured via environment variables, and more than
# one of these containers can still be run.
###
FROM php:8.4-cli-alpine3.21
# Default to latest, but should pass in a version
ARG APP_VERSION="latest"
RUN docker-php-ext-install pdo_mysql
# Start with our base worker image
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
COPY --chown=www-data:www-data . /app
# Set the APP_VERSION in the image
ENV APP_VERSION=${APP_VERSION}
# Copy the actual application code from the previously built app
COPY --chown=1000:1000 ./ /app
# To retain backwards compatibility, default to async & download transports
CMD [ "--monitor" ]
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]

3
docker/Dockerfile.web Normal file
View File

@@ -0,0 +1,3 @@
FROM nginx:1.28-alpine
COPY bash/nginx.conf /etc/nginx/conf.d/default.conf

View File

@@ -1,21 +1,7 @@
###
# This version of Torsearch can run the scheduler, downloader, and
# worker in one container. Each process is managerd by supervisord
# and can be configured via environment variables, and more than
# one of these containers can still be run.
###
FROM php:8.4-cli-alpine3.21
# Default to latest, but should pass in a version
ARG APP_VERSION="latest"
RUN docker-php-ext-install pdo_mysql
# Start with our base worker image
FROM code.caldwell.digital/home/torsearch-base-worker-supervisord:latest
COPY --chown=www-data:www-data . /app
# Set the APP_VERSION in the image
ENV APP_VERSION=${APP_VERSION}
# Copy the actual application code from the previously built app
COPY --chown=1000:1000 ./ /app
# To retain backwards compatibility, default to async & download transports
CMD [ "--async", "--download" ]
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]

View File

@@ -1,59 +0,0 @@
{
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
{$CADDY_SERVER_LOG_OPTIONS}
# Redact the authorization query parameter that can be set by Mercure
format filter {
request>uri query {
replace authorization REDACTED
}
}
}
root /app/public
encode zstd br gzip
mercure {
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
# Allow anonymous subscribers (double-check that it's what you want)
anonymous
# Enable the subscription API (double-check that it's what you want)
subscriptions
# Custmo cors
cors_origins *
# Extra directives
{$MERCURE_EXTRA_DIRECTIVES}
}
vulcain
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
@phpRoute {
not path /.well-known/mercure*
not file {path}
}
rewrite @phpRoute index.php
@frontController path index.php
php @frontController
file_server {
hide *.php
}
}

View File

@@ -1,12 +0,0 @@
#!/bin/sh
# Sleep for a second to ensure DB is awake and ready
SLEEP_TIME=$(shuf -i 2-4 -n 1)
echo "> Sleeping for ${SLEEP_TIME} seconds to wait for the database"
sleep $SLEEP_TIME
# Provision database
php /app/bin/console doctrine:migrations:migrate --no-interaction
php /app/bin/console db:seed
exec docker-php-entrypoint "$@"

View File

@@ -1,12 +0,0 @@
[Unit]
Description=Torsearch media cache warming services
[Service]
ExecStart=php /app/bin/console messenger:consume media_cache --time-limit=3600
# for Redis, set a custom consumer name for each instance
Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i"
Restart=always
RestartSec=30
[Install]
WantedBy=default.target

View File

@@ -1,10 +0,0 @@
[program:torsearch-worker]
command=/app/bin/console messenger:consume async -vv --time-limit 3600
numprocs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
startretries=3
process_name=%(program_name)s_%(process_num)02d

View File

@@ -1,10 +0,0 @@
[program:torsearch-downloader]
command=/app/bin/console messenger:consume download -vv --time-limit 3600
numprocs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
startretries=3
process_name=%(program_name)s_%(process_num)02d

View File

@@ -1,10 +0,0 @@
[program:torsearch-scheduler]
command=/app/bin/console messenger:consume scheduler_monitor monitor -vv --time-limit 21600
numprocs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
startretries=3
process_name=%(program_name)s_%(process_num)02d

View File

@@ -1,5 +0,0 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid

View File

@@ -3,15 +3,10 @@
# or pass your certificates into the 'app' container.
# Please omit any trailing slashes. The APP_URL is
# used to generate the Mercure URL behind the scenes.
APP_URL="https://dev.caldwell.digital"
APP_URL="https://torsearch.idocode.io"
APP_SECRET="70169beadfbc8101c393cbfbba27a313"
APP_ENV=prod
# Mercure is a Caddy module built into the webserver
# that facilitates the usage of websockets to transmit
# real time data (download progress, etc.)
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
# Use the DATABASE_URL below to use the MariaDB container
# provided in the example.compose.yml file, or remove this
# line and fill in the details of your own MySQL/MariaDB server
@@ -26,46 +21,33 @@ DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-Ma
# to retrieve download options
REAL_DEBRID_KEY=""
# Enter your TMDB API key
# Enter you TMDB API key
# This is used to provide rich search results
# when searching for media and rendering the
# Popular Movies and TV Shows section.
TMDB_API=""
TMDB_API=
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
# Use your own Redis instance or use the
# below value to use the container included
# in the example compose.yml file.
REDIS_HOST="redis://redis"
### Auth ###
# Change to "oidc" to and provide the required
# environment variables below to use OIDC auth.
AUTH_METHOD=form_login
# OIDC
OIDC_WELL_KNOWN_URL=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
# Allows you to skip the login page and directly
# rely on your IdP for auth.
OIDC_BYPASS_FORM_LOGIN=
# LDAP Config: To use LDAP, enter the below fields
# and run 'php bin/console config:set auth.method ldap'
# (LDAP is still in progress and not ready for use)
#LDAP_HOST=
#LDAP_PORT=
#LDAP_ENCRYPTION=
#LDAP_BASE_DN=
#LDAP_BIND_USER=
#LDAP_BIND_PASS=
#LDAP_DN_STRING=
#LDAP_UID_KEY="uid"
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=""
#LDAP_EMAIL_ATTRIBUTE=mail
#LDAP_USERNAME_ATTRIBUTE=uid
#LDAP_NAME_ATTRIBUTE=displayname
LDAP_ADMIN_ROLE_DN=""
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_USERNAME_ATTRIBUTE=uid
LDAP_NAME_ATTRIBUTE=displayname

View File

@@ -1,17 +1,16 @@
services:
# The "entrypoint" into the application. This reverse proxy
# proxies traffic back to their respective services. If not
# running behind a reverse proxy inject your SSL certificates
# into this container.
# This container runs the actual web app in a php:8.4-fpm
# base container.
app:
image: code.caldwell.digital/home/torsearch-app:latest
ports:
- '8006:80'
env_file:
- .env
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on:
database:
condition: service_healthy
@@ -26,11 +25,9 @@ services:
worker:
image: code.caldwell.digital/home/torsearch-worker:latest
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
command: -vv --time-limit=3600 --limit=10
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
command: -v --time-limit=3600 --limit=10
env_file:
- .env
restart: always
@@ -46,18 +43,36 @@ services:
scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
- ./downloads:/var/download
command: -vv --time-limit=3600
env_file:
- .env
command: -vv
environment:
TZ: America/Chicago
restart: always
depends_on:
app:
condition: service_healthy
# This container facilitates viewing the progress of downloads
# in realtime. It also handles sending alerts and notifications.
# The MERCURE_PUBLISHER_JWT key & MERCURE_SUBSCRIBER_JWT_KEY should
# match the MERCURE_JWT_SECRET environment variable.
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3001:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database:
image: mariadb:10.11.2
volumes:
@@ -69,8 +84,7 @@ services:
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
timeout: 10s
retries: 10
redis:

View File

@@ -1,115 +0,0 @@
services:
# The "entrypoint" into the application. This reverse proxy
# proxies traffic back to their respective services. If not
# running behind a reverse proxy inject your SSL certificates
# into this container.
# This container runs the actual web app in a php:8.4-fpm
# base container.
app:
image: code.caldwell.digital/home/torsearch-app:latest
ports:
- '8006:80'
configs:
- env_file
deploy:
replicas: 2
depends_on:
- database
# Downloads happen in this container. Replicate this
# container to run multiple downloads simultaneously.
# Map your "movies" folder to /var/download/movies
# Map your "TV shows" folder to /var/download/tvshows
# If your folders are on another machine, use an NFS volume.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
worker:
image: code.caldwell.digital/home/torsearch-worker:latest
configs:
- source: env_file
target: /app/bin/.env.local
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
restart: always
command: -vv
deploy:
replicas: 4
depends_on:
- app
# This container handles the monitoring for new media. When new
# monitors are added, jobs are periodically dispatched to this
# container, and the desired media is searched for and downloaded.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest
configs:
- env_file
volumes:
- ./downloads:/var/download
restart: always
depends_on:
- app
# This container facilitates viewing the progress of downloads
# in realtime. It also handles sending alerts and notifications.
# The MERCURE_PUBLISHER_JWT key & MERCURE_SUBSCRIBER_JWT_KEY should
# match the MERCURE_JWT_SECRET environment variable.
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3001:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database:
image: mariadb:10.11.2
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
# **Optional**
# Provides a simple method of viewing the database
adminer:
image: adminer
ports:
- "8081:8080"
volumes:
mysql:
mercure_config:
mercure_data:
redis_data:
configs:
env_file:
file: $PWD/.env

View File

@@ -28,46 +28,4 @@ return [
'@hotwired/turbo' => [
'version' => '7.3.0',
],
'@stimulus-components/popover' => [
'version' => '7.0.0',
],
'@stimulus-components/dialog' => [
'version' => '1.0.1',
],
'@stimulus-components/dropdown' => [
'version' => '3.0.0',
],
'stimulus-use' => [
'version' => '0.52.2',
],
'animate.css' => [
'version' => '4.1.1',
],
'animate.css/animate.min.css' => [
'version' => '4.1.1',
'type' => 'css',
],
'tom-select' => [
'version' => '2.4.3',
],
'@orchidjs/sifter' => [
'version' => '1.1.0',
],
'@orchidjs/unicode-variants' => [
'version' => '1.1.2',
],
'tom-select/dist/css/tom-select.default.min.css' => [
'version' => '2.4.3',
'type' => 'css',
],
'tom-select/dist/css/tom-select.default.css' => [
'version' => '2.4.3',
'type' => 'css',
],
'pulltorefreshjs' => [
'version' => '0.1.22',
],
'@ungap/custom-elements' => [
'version' => '1.3.0',
],
];

View File

@@ -1,35 +0,0 @@
<?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 Version20250610195448 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 created_at DATETIME NULL, ADD updated_at DATETIME NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE download DROP created_at, DROP updated_at
SQL);
}
}

View File

@@ -1,50 +0,0 @@
<?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 Version20250610222503 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 monitor ADD parent_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor ADD CONSTRAINT FK_E1159985727ACA70 FOREIGN KEY (parent_id) REFERENCES monitor (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_E1159985727ACA70 ON monitor (parent_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 monitor DROP FOREIGN KEY FK_E1159985727ACA70
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_E1159985727ACA70 ON monitor
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor DROP parent_id
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE download CHANGE created_at created_at DATETIME DEFAULT NULL, CHANGE updated_at updated_at DATETIME DEFAULT NULL
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?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 Version20250708033046 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 monitor ADD only_future TINYINT(1) NOT NULL DEFAULT 1
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE monitor DROP only_future
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?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 Version20250709161037 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 CHANGE batch_id episode_id 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 download CHANGE episode_id batch_id VARCHAR(255) DEFAULT NULL
SQL);
}
}

View File

@@ -1,47 +0,0 @@
<?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 Version20250709200956 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 reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', expires_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_7CE748AA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor CHANGE only_future only_future TINYINT(1) NOT 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 reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395
SQL);
$this->addSql(<<<'SQL'
DROP TABLE reset_password_request
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE monitor CHANGE only_future only_future TINYINT(1) DEFAULT 1 NOT NULL
SQL);
}
}

View File

@@ -1,41 +0,0 @@
<?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 Version20250724042107 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 preference_option DROP FOREIGN KEY FK_607C52FD81022C0
SQL);
$this->addSql(<<<'SQL'
DROP TABLE preference_option
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE preference_option (id INT AUTO_INCREMENT NOT NULL, preference_id VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, value VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, enabled TINYINT(1) NOT NULL, INDEX IDX_607C52FD81022C0 (preference_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = ''
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE preference_option ADD CONSTRAINT FK_607C52FD81022C0 FOREIGN KEY (preference_id) REFERENCES preference (id)
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?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 Version20250823173128 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 monitor ADD air_date DATETIME DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE monitor DROP air_date
SQL);
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250831013403 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP TABLE IF EXISTS sessions
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE sessions (sess_id VARBINARY(128) NOT NULL, sess_data LONGBLOB NOT NULL, sess_lifetime INT UNSIGNED NOT NULL, sess_time INT UNSIGNED NOT NULL, INDEX sess_lifetime_idx (sess_lifetime), PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_bin` ENGINE = InnoDB COMMENT = ''
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251101194723 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(255) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE user_preference CHANGE preference_value preference_value VARCHAR(1024) DEFAULT NULL
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251101211617 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE event_log (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) DEFAULT NULL, message LONGTEXT DEFAULT NULL, context JSON DEFAULT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP TABLE event_log
SQL);
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251102004627 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log ADD user_id INT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE event_log ADD CONSTRAINT FK_9EF0AD16A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9EF0AD16A76ED395 ON event_log (user_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log DROP FOREIGN KEY FK_9EF0AD16A76ED395
SQL);
$this->addSql(<<<'SQL'
DROP INDEX IDX_9EF0AD16A76ED395 ON event_log
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE event_log DROP user_id
SQL);
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251102221034 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log ADD created_at DATETIME NULL, ADD updated_at DATETIME NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE event_log DROP created_at, DROP updated_at
SQL);
}
}

View File

@@ -1,129 +0,0 @@
variable "image_tag" {
type = string
description = "Docker image tag to deploy."
default = "latest"
}
job "torsearch" {
datacenters = [ "home" ]
type = "service"
group "app" {
count = 2
update {
max_parallel = 4
min_healthy_time = "30s"
healthy_deadline = "3m"
auto_revert = true
}
network {
port "app" {
to = 80
}
}
task "app" {
driver = "docker"
config {
image = "registry.caldwell.digital/home/torsearch-app:${var.image_tag}"
ports = ["app"]
}
env {
MERCURE_PUBLISHER_JWT_KEY = "!ChangeThisMercureHubJWTSecretKey!"
MERCURE_SUBSCRIBER_JWT_KEY = "!ChangeThisMercureHubJWTSecretKey!"
}
service {
name = "torsearch-app"
provider = "nomad"
port = "app"
meta {
nomad_ingress_enabled = true
nomad_ingress_hostname = "torsearch-nomad.caldwell.digital"
}
}
}
}
group "worker" {
count = 2
update {
max_parallel = 4
min_healthy_time = "30s"
healthy_deadline = "3m"
auto_revert = true
}
volume "media" {
type = "host"
source = "media"
read_only = false
}
task "worker" {
driver = "docker"
volume_mount {
volume = "media"
destination = "/var/download"
read_only = false
}
config {
image = "registry.caldwell.digital/home/torsearch-worker:${var.image_tag}"
args = [
"-vv"
]
}
service {
name = "torsearch-worker"
provider = "nomad"
}
}
}
group "scheduler" {
count = 1
update {
max_parallel = 2
min_healthy_time = "30s"
healthy_deadline = "3m"
auto_revert = true
}
volume "media" {
type = "host"
source = "media"
read_only = false
}
task "scheduler" {
driver = "docker"
volume_mount {
volume = "media"
destination = "/var/download"
read_only = false
}
config {
image = "registry.caldwell.digital/home/torsearch-scheduler:${var.image_tag}"
args = [
"-vv"
]
}
service {
name = "torsearch-scheduler"
provider = "nomad"
}
}
}
}

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
</extensions>
</phpunit>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More