Compare commits
1 Commits
dev-web-co
...
dev-php85-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0964abdf37 |
15
Dockerfile
15
Dockerfile
@@ -1,16 +1,17 @@
|
|||||||
FROM dunglas/frankenphp:php8.4
|
FROM registry.caldwell.digital/home/frankenphp:1.9.0-php8.5.0-ubuntu
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
ENV SERVER_NAME=":80"
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
||||||
ENV APP_VERSION="0.0.1"
|
ENV APP_VERSION="0.0.1"
|
||||||
|
ENV SERVER_ROOT="/app/public"
|
||||||
|
|
||||||
RUN install-php-extensions \
|
#RUN install-php-extensions \
|
||||||
pdo_mysql \
|
# pdo_mysql \
|
||||||
gd \
|
# gd \
|
||||||
intl \
|
# intl \
|
||||||
zip \
|
# zip \
|
||||||
opcache
|
# opcache
|
||||||
|
|
||||||
RUN apt update && apt install -y wget
|
RUN apt update && apt install -y wget
|
||||||
|
|
||||||
|
|||||||
15
assets/bootstrap.js
vendored
15
assets/bootstrap.js
vendored
@@ -1,19 +1,10 @@
|
|||||||
import EpisodeContainer from './components/episode-container.js';
|
|
||||||
import DownloadOptionTr from './components/download-option-tr.js';
|
|
||||||
import MovieContainer from "./components/movie-container.js";
|
|
||||||
|
|
||||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||||
import Popover from '@stimulus-components/popover';
|
import Popover from '@stimulus-components/popover'
|
||||||
import Dialog from '@stimulus-components/dialog';
|
import Dialog from '@stimulus-components/dialog'
|
||||||
import Dropdown from '@stimulus-components/dropdown';
|
import Dropdown from '@stimulus-components/dropdown'
|
||||||
import 'animate.css';
|
|
||||||
|
|
||||||
const app = startStimulusApp();
|
const app = startStimulusApp();
|
||||||
// register any custom, 3rd party controllers here
|
// register any custom, 3rd party controllers here
|
||||||
app.register('popover', Popover);
|
app.register('popover', Popover);
|
||||||
app.register('dialog', Dialog);
|
app.register('dialog', Dialog);
|
||||||
app.register('dropdown', Dropdown);
|
app.register('dropdown', Dropdown);
|
||||||
|
|
||||||
customElements.define('episode-container', EpisodeContainer);
|
|
||||||
customElements.define('movie-container', MovieContainer);
|
|
||||||
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
export default class DownloadOptionTr extends HTMLTableRowElement {
|
|
||||||
H264_CODECS = ['h264', 'h.264', 'x264']
|
|
||||||
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
|
||||||
|
|
||||||
#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']}"]`)
|
|
||||||
const props = {
|
|
||||||
"resolution": this.resolution.trim(),
|
|
||||||
"codec": this.codec.trim(),
|
|
||||||
"provider": this.provider.trim(),
|
|
||||||
"languages": this.languages,
|
|
||||||
"quality": this.quality,
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,9 @@ import { Controller } from '@hotwired/stimulus';
|
|||||||
*/
|
*/
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
H264_CODECS = ['h264', 'h.264', 'x264']
|
||||||
|
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
title: String,
|
title: String,
|
||||||
tmdbId: String,
|
tmdbId: String,
|
||||||
@@ -27,8 +30,73 @@ export default class extends Controller {
|
|||||||
this.optionsLoaded = true;
|
this.optionsLoaded = true;
|
||||||
this.options = this.element.querySelectorAll('tbody tr');
|
this.options = this.element.querySelectorAll('tbody tr');
|
||||||
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
|
||||||
this.resultCountEl.innerText = this.options.length;
|
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||||
this.loadingIconOutlet.toggleIcon();
|
this.loadingIconOutlet.toggleIcon();
|
||||||
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
|
this.resultCountEl.innerText = this.options.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 optionHeader = document.querySelector(`[data-option-id="${option.dataset['localId']}"]`)
|
||||||
|
const props = {
|
||||||
|
"resolution": option.querySelector('#resolution').textContent.trim(),
|
||||||
|
"codec": option.querySelector('#codec').textContent.trim(),
|
||||||
|
"provider": option.querySelector('#provider').textContent.trim(),
|
||||||
|
"quality": option.dataset['quality'],
|
||||||
|
"languages": JSON.parse(option.dataset['languages']),
|
||||||
|
}
|
||||||
|
|
||||||
|
let include = true;
|
||||||
|
option.classList.add('r-tablerow');
|
||||||
|
option.classList.remove('hidden');
|
||||||
|
optionHeader.classList.add('r-tablerow');
|
||||||
|
optionHeader.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.remove('r-tablerow');
|
||||||
|
option.classList.add('hidden');
|
||||||
|
optionHeader.classList.remove('r-tablerow');
|
||||||
|
optionHeader.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.resultCountEl.innerText = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default class extends Controller {
|
|||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.element.parentElement.classList.toggle('hidden');
|
this.element.parentElement.classList.toggle('hidden');
|
||||||
this.element.classList.toggle('animate__slideInLeft');
|
|
||||||
this.element.classList.toggle('fixed');
|
this.element.classList.toggle('fixed');
|
||||||
this.element.classList.toggle('z-20');
|
this.element.classList.toggle('z-20');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { Controller } from '@hotwired/stimulus';
|
|||||||
*/
|
*/
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
H264_CODECS = ['h264', 'h.264', 'x264']
|
||||||
|
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
||||||
|
|
||||||
languages = []
|
languages = []
|
||||||
providers = []
|
providers = []
|
||||||
qualities = []
|
qualities = []
|
||||||
@@ -19,7 +22,7 @@ export default class extends Controller {
|
|||||||
"quality": "",
|
"quality": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
static outlets = ['tv-episode-list']
|
static outlets = ['movie-results', 'tv-results', 'tv-episode-list']
|
||||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
||||||
static values = {
|
static values = {
|
||||||
'imdbId': String,
|
'imdbId': String,
|
||||||
@@ -33,34 +36,20 @@ export default class extends Controller {
|
|||||||
this.activeFilter['season'] = 1;
|
this.activeFilter['season'] = 1;
|
||||||
}
|
}
|
||||||
await this.filter();
|
await this.filter();
|
||||||
|
|
||||||
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event is fired from movies/tvshows controllers to populate this data
|
// Event is fired from movies/tvshows controllers to populate this data
|
||||||
async loadOptions({detail: { options }}) {
|
async loadOptions({detail: { options }}) {
|
||||||
await options.forEach((option) => {
|
await options.forEach((option) => {
|
||||||
this.addLanguages(option);
|
this.addLanguages(option, option.dataset);
|
||||||
this.addProviders(option);
|
this.addProviders(option, option.dataset);
|
||||||
this.addQualities(option);
|
this.addQualities(option, option.dataset);
|
||||||
})
|
})
|
||||||
await this.filter();
|
await this.filter();
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAllEpisodes() {
|
addLanguages(option, props) {
|
||||||
document.dispatchEvent(new CustomEvent('selectEpisodeForDownload', {
|
const languages = Object.assign([], JSON.parse(props['languages']));
|
||||||
detail: {
|
|
||||||
select: this.selectAllTarget.checked,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSelectedEpisodes() {
|
|
||||||
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
|
|
||||||
}
|
|
||||||
|
|
||||||
addLanguages(option) {
|
|
||||||
const languages = Object.assign([], option.languages);
|
|
||||||
languages.forEach((language) => {
|
languages.forEach((language) => {
|
||||||
if (!this.languages.includes(language)) {
|
if (!this.languages.includes(language)) {
|
||||||
this.languages.push(language);
|
this.languages.push(language);
|
||||||
@@ -86,9 +75,9 @@ export default class extends Controller {
|
|||||||
.join();
|
.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
addProviders(option) {
|
addProviders(option, props) {
|
||||||
if (!this.providers.includes(option.provider)) {
|
if (!this.providers.includes(props['provider'])) {
|
||||||
this.providers.push(option.provider);
|
this.providers.push(props['provider']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferred = this.providerTarget.dataset.preferred;
|
const preferred = this.providerTarget.dataset.preferred;
|
||||||
@@ -111,10 +100,10 @@ export default class extends Controller {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addQualities(option) {
|
addQualities(option, props) {
|
||||||
if (!this.qualities.includes(option.quality)) {
|
if (!this.qualities.includes(props['quality'])) {
|
||||||
if (option.quality.toLowerCase() in this.reverseMappedQualitiesValue) {
|
if (props['quality'].toLowerCase() in this.reverseMappedQualitiesValue) {
|
||||||
this.qualities.push(option.quality);
|
this.qualities.push(props['quality']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +128,9 @@ export default class extends Controller {
|
|||||||
|
|
||||||
async filter() {
|
async filter() {
|
||||||
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
|
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
|
||||||
|
const currentSeason = this.activeFilter['season'];
|
||||||
|
|
||||||
|
let results = [];
|
||||||
this.activeFilter = {
|
this.activeFilter = {
|
||||||
"resolution": this.resolutionTarget.value,
|
"resolution": this.resolutionTarget.value,
|
||||||
"codec": this.codecTarget.value,
|
"codec": this.codecTarget.value,
|
||||||
@@ -148,26 +139,26 @@ export default class extends Controller {
|
|||||||
"quality": this.qualityTarget.value,
|
"quality": this.qualityTarget.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("tvshows" === this.mediaTypeValue) {
|
if ("movies" === this.mediaTypeValue) {
|
||||||
downloadSeasonSpan.innerText = this.seasonTarget.value;
|
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;
|
this.activeFilter.season = this.seasonTarget.value;
|
||||||
|
downloadSeasonSpan.innerText = this.activeFilter.season;
|
||||||
|
await results.forEach((list) => list.filter(this.activeFilter, currentSeason, 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) {
|
setSeason(event) {
|
||||||
this.tvEpisodeListOutlet.setSeason(event.target.value);
|
this.tvEpisodeListOutlet.setSeason(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uncheckSelectAllBtn() {
|
||||||
|
this.selectAllTarget.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
downloadSeason() {
|
downloadSeason() {
|
||||||
fetch(`/api/download/season/${this.imdbIdValue}/${this.activeFilter['season']}`, {
|
fetch(`/api/download/season/${this.imdbIdValue}/${this.activeFilter['season']}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -175,4 +166,21 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ export default class extends Controller {
|
|||||||
this.component.on('render:finished', (component) => {
|
this.component.on('render:finished', (component) => {
|
||||||
console.log(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) {
|
setSeason(season) {
|
||||||
@@ -32,7 +25,6 @@ export default class extends Controller {
|
|||||||
|
|
||||||
paginate(event) {
|
paginate(event) {
|
||||||
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
|
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
|
||||||
this.component.set('episodeNumber', null);
|
|
||||||
this.component.action('paginate', {page: event.params.page});
|
this.component.action('paginate', {page: event.params.page});
|
||||||
this.component.render();
|
this.component.render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,23 +18,149 @@ export default class extends Controller {
|
|||||||
active: Boolean,
|
active: Boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
static targets = ['list', 'count', 'episodeSelector',]
|
static targets = ['list', 'count', 'episodeSelector', 'toggleButton', 'listContainer']
|
||||||
static outlets = ['loading-icon']
|
static outlets = ['loading-icon']
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
|
optionsLoaded = false
|
||||||
|
isOpen = false
|
||||||
|
|
||||||
listTargetConnected() {
|
async listTargetConnected() {
|
||||||
this.element.options = this.element.querySelectorAll('tbody tr');
|
this.options = this.element.querySelectorAll('tbody tr');
|
||||||
if (this.element.options.length > 0) {
|
if (this.options.length > 0) {
|
||||||
this.element.options.forEach((option) =>
|
this.options.forEach((option) =>
|
||||||
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
||||||
);
|
);
|
||||||
this.element.options[0].querySelector('input[type="checkbox"]').checked = true;
|
this.options[0].querySelector('input[type="checkbox"]').checked = true;
|
||||||
|
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
||||||
this.loadingIconOutlet.increaseCount();
|
this.loadingIconOutlet.increaseCount();
|
||||||
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}}));
|
|
||||||
} else {
|
} else {
|
||||||
this.countTarget.innerText = 0;
|
this.countTarget.innerText = 0;
|
||||||
this.episodeSelectorTarget.disabled = true;
|
this.episodeSelectorTarget.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// async clearCache() {
|
||||||
|
// await fetch(`/torrentio/tvshows/clear/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
||||||
|
// .then(res => res.text())
|
||||||
|
// .then(response => {});
|
||||||
|
// }
|
||||||
|
|
||||||
|
async setActive() {
|
||||||
|
if (false === this.optionsLoaded) {
|
||||||
|
await this.setOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInActive() {
|
||||||
|
this.episodeSelectorTarget.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('options-table');
|
||||||
|
this.listTarget.classList.toggle('hidden');
|
||||||
|
this.toggleButtonTarget.classList.toggle('rotate-90');
|
||||||
|
this.toggleButtonTarget.classList.toggle('-rotate-90');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 optionHeader = document.querySelector(`[data-option-id="${option.dataset['localId']}"]`)
|
||||||
|
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']),
|
||||||
|
"quality": option.dataset['quality'],
|
||||||
|
}
|
||||||
|
|
||||||
|
let include = true;
|
||||||
|
option.classList.add('r-tablerow');
|
||||||
|
option.classList.remove('hidden');
|
||||||
|
optionHeader.classList.add('r-tablerow');
|
||||||
|
optionHeader.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.remove('r-tablerow');
|
||||||
|
option.classList.add('hidden');
|
||||||
|
optionHeader.classList.remove('r-tablerow');
|
||||||
|
optionHeader.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-input {
|
.text-input {
|
||||||
@apply bg-gray-50 text-gray-50 px-2 py-1 bg-transparent border-b-2 border-orange-400
|
@apply bg-gray-50 text-gray-50 p-1 bg-transparent border-b-2 border-orange-400
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
@@ -130,21 +130,3 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
background: unset;
|
background: unset;
|
||||||
@apply bg-orange-500/80 text-black font-bold rounded-md
|
@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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ services:
|
|||||||
- mercure_data:/data
|
- mercure_data:/data
|
||||||
- mercure_config:/config
|
- mercure_config:/config
|
||||||
tty: true
|
tty: true
|
||||||
|
ports:
|
||||||
|
- "8001:80"
|
||||||
|
command: "frankenphp php-server --root=/app/public"
|
||||||
environment:
|
environment:
|
||||||
TZ: America/Chicago
|
TZ: America/Chicago
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ pwa:
|
|||||||
theme_color: "#083344"
|
theme_color: "#083344"
|
||||||
description: Torsearch provides a simple and intuitive way to manage your personal media library.
|
description: Torsearch provides a simple and intuitive way to manage your personal media library.
|
||||||
icons:
|
icons:
|
||||||
- src: "/icon.png"
|
- src: "icon.png"
|
||||||
sizes: [ 192 ]
|
sizes: [ 192 ]
|
||||||
- src: "/icon.png"
|
- src: "icon.png"
|
||||||
sizes: [ 192 ]
|
sizes: [ 192 ]
|
||||||
purpose: maskable
|
purpose: maskable
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
frankenphp {
|
frankenphp {
|
||||||
{$FRANKENPHP_CONFIG}
|
{$FRANKENPHP_CONFIG}
|
||||||
|
num_threads 10
|
||||||
|
max_threads 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
public/test.php
Normal file
60
public/test.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once '../vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Torrentio\Result\ResultFactory;
|
||||||
|
|
||||||
|
$realDebridKey = "";
|
||||||
|
$tasks = [];
|
||||||
|
$results = [];
|
||||||
|
$start = microtime(true);
|
||||||
|
for ($i = 1; $i <= 20; $i++) {
|
||||||
|
$tasks[] = \Async\spawn(function () use ($i, &$results, &$realDebridKey) {
|
||||||
|
$baseUrl = "https://torrentio.strem.fun/providers%253Dyts%252Ceztv%252Crarbg%252C1337x%252Cthepiratebay%252Ckickasstorrents%252Ctorrentgalaxy%252Cmagnetdl%252Chorriblesubs%252Cnyaasi%7Csort%253Dqualitysize%7Cqualityfilter%253D480p%252Cscr%252Ccam%7Crealdebrid={$realDebridKey}/stream/movie/tt0412142:1:$i.json";
|
||||||
|
$options = \json_decode(file_get_contents($baseUrl), true);
|
||||||
|
|
||||||
|
foreach ($options['streams'] as $stream) {
|
||||||
|
if (!str_starts_with($stream['url'], "https")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
array_key_exists('behaviorHints', $stream) &&
|
||||||
|
array_key_exists('bingeGroup', $stream['behaviorHints'])
|
||||||
|
) {
|
||||||
|
$bingeGroup = $stream['behaviorHints']['bingeGroup'];
|
||||||
|
} else {
|
||||||
|
$bingeGroup = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ResultFactory::map(
|
||||||
|
$stream['url'],
|
||||||
|
$stream['title'],
|
||||||
|
$bingeGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
$results[] = $result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
\Async\awaitAll($tasks);
|
||||||
|
$end = microtime(true) - $start;
|
||||||
|
dd($end, $results);
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// Spawn multiple concurrent coroutines
|
||||||
|
//Async\spawn(function() {
|
||||||
|
// echo "Starting coroutine 1\n";
|
||||||
|
// sleep(2); // Non-blocking in async context
|
||||||
|
// echo "Coroutine 1 completed\n";
|
||||||
|
//});
|
||||||
|
//
|
||||||
|
//Async\spawn(function() {
|
||||||
|
// echo "Starting coroutine 2\n";
|
||||||
|
// sleep(1); // Non-blocking in async context
|
||||||
|
// echo "Coroutine 2 completed\n";
|
||||||
|
//});
|
||||||
|
//
|
||||||
|
//echo "All coroutines started\n";
|
||||||
|
|
||||||
@@ -20,15 +20,17 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||||||
class SeedDatabaseCommand extends Command
|
class SeedDatabaseCommand extends Command
|
||||||
{
|
{
|
||||||
private PreferencesRepository $preferenceRepository;
|
private PreferencesRepository $preferenceRepository;
|
||||||
|
private PreferenceOptionRepository $preferenceOptionRepository;
|
||||||
private UserRepository $userRepository;
|
private UserRepository $userRepository;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
PreferencesRepository $preferenceRepository,
|
PreferencesRepository $preferenceRepository,
|
||||||
|
PreferenceOptionRepository $preferenceOptionRepository,
|
||||||
UserRepository $userRepository,
|
UserRepository $userRepository,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->preferenceRepository = $preferenceRepository;
|
$this->preferenceRepository = $preferenceRepository;
|
||||||
|
$this->preferenceOptionRepository = $preferenceOptionRepository;
|
||||||
$this->userRepository = $userRepository;
|
$this->userRepository = $userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ class SeedDatabaseCommand extends Command
|
|||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
$this->seedPreferences($io);
|
$this->seedPreferences($io);
|
||||||
|
$this->seedPreferenceOptions($io);
|
||||||
$this->updateUserPreferences($io);
|
$this->updateUserPreferences($io);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
@@ -137,4 +140,72 @@ class SeedDatabaseCommand extends Command
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function seedPreferenceOptions(SymfonyStyle $io)
|
||||||
|
{
|
||||||
|
$io->info('[SeedDatabaseCommand] > Seeding preference options...');
|
||||||
|
$options = $this->getPreferenceOptions();
|
||||||
|
|
||||||
|
foreach ($options as $option) {
|
||||||
|
if ($this->preferenceOptionRepository->findBy([
|
||||||
|
'preference' => $option['preference_id'],
|
||||||
|
'name' => $option['name'],
|
||||||
|
'value' => $option['value'],
|
||||||
|
'enabled' => $option['enabled'],
|
||||||
|
])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->preferenceOptionRepository->getEntityManager()->persist(
|
||||||
|
(new \App\User\Framework\Entity\PreferenceOption())
|
||||||
|
->setPreference($this->preferenceRepository->find($option['preference_id']))
|
||||||
|
->setName($option['name'])
|
||||||
|
->setValue($option['value'])
|
||||||
|
->setEnabled($option['enabled'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preferenceOptionRepository->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPreferenceOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'preference_id' => 'resolution',
|
||||||
|
'name' => '720p',
|
||||||
|
'value' => '720p',
|
||||||
|
'enabled' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'preference_id' => 'resolution',
|
||||||
|
'name' => '1080p',
|
||||||
|
'value' => '1080p',
|
||||||
|
'enabled' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'preference_id' => 'resolution',
|
||||||
|
'name' => '2160p',
|
||||||
|
'value' => '2160p',
|
||||||
|
'enabled' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'preference_id' => 'codec',
|
||||||
|
'name' => '-',
|
||||||
|
'value' => '-',
|
||||||
|
'enabled' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'preference_id' => 'codec',
|
||||||
|
'name' => 'h264',
|
||||||
|
'value' => 'h264',
|
||||||
|
'enabled' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'preference_id' => 'codec',
|
||||||
|
'name' => 'h265/HEVC',
|
||||||
|
'value' => 'h265',
|
||||||
|
'enabled' => true
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ class GetMediaInfoCommand implements CommandInterface
|
|||||||
public string $imdbId,
|
public string $imdbId,
|
||||||
public string $mediaType,
|
public string $mediaType,
|
||||||
public ?int $season = null,
|
public ?int $season = null,
|
||||||
public ?int $episode = null,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,6 @@ class GetMediaInfoHandler implements HandlerInterface
|
|||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||||
|
|
||||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
return new GetMediaInfoResult($media, $command->season);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ class GetMediaInfoInput implements InputInterface
|
|||||||
|
|
||||||
#[SourceRoute('season', nullify: true)]
|
#[SourceRoute('season', nullify: true)]
|
||||||
public ?int $season,
|
public ?int $season,
|
||||||
|
|
||||||
#[SourceRoute('episode', nullify: true)]
|
|
||||||
public ?int $episode,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function toCommand(): CommandInterface
|
public function toCommand(): CommandInterface
|
||||||
@@ -29,10 +26,6 @@ class GetMediaInfoInput implements InputInterface
|
|||||||
if ("tvshows" === $this->mediaType && null === $this->season) {
|
if ("tvshows" === $this->mediaType && null === $this->season) {
|
||||||
$this->season = 1;
|
$this->season = 1;
|
||||||
}
|
}
|
||||||
|
return new GetMediaInfoCommand($this->imdbId, $this->mediaType, $this->season);
|
||||||
if ("tvshows" === $this->mediaType && null === $this->episode) {
|
|
||||||
$this->episode = 1;
|
|
||||||
}
|
|
||||||
return new GetMediaInfoCommand($this->imdbId, $this->mediaType, $this->season, $this->episode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,5 @@ class GetMediaInfoResult implements ResultInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $media,
|
public TmdbResult $media,
|
||||||
public ?int $season,
|
public ?int $season,
|
||||||
public ?int $episode,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class WebController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/result/{mediaType}/{imdbId}/{season}/{episode?}', name: 'app_search_result')]
|
#[Route('/result/{mediaType}/{imdbId}/{season}', name: 'app_search_result')]
|
||||||
public function result(
|
public function result(
|
||||||
GetMediaInfoInput $input,
|
GetMediaInfoInput $input,
|
||||||
?int $season = null,
|
?int $season = null,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Tmdb;
|
namespace App\Tmdb;
|
||||||
|
|
||||||
|
use \Async;
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Base\Enum\MediaType;
|
use App\Base\Enum\MediaType;
|
||||||
use App\ValueObject\ResultFactory;
|
use App\ValueObject\ResultFactory;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Contracts\Cache\ItemInterface;
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
@@ -42,6 +44,7 @@ class Tmdb
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CacheItemPoolInterface $cache,
|
private readonly CacheItemPoolInterface $cache,
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
#[Autowire(env: 'TMDB_API')] string $apiKey,
|
||||||
) {
|
) {
|
||||||
$this->client = new Client(
|
$this->client = new Client(
|
||||||
@@ -214,7 +217,6 @@ class Tmdb
|
|||||||
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
|
if ($season['episode_count'] <= 0 || $season['name'] === 'Specials') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$series['episodes'][$season['season_number']] = Map::from(
|
$series['episodes'][$season['season_number']] = Map::from(
|
||||||
$client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
|
$client->getApi()->getSeason($series['id'], $season['season_number'])['episodes']
|
||||||
)->map(function ($data) {
|
)->map(function ($data) {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class GetTvShowOptionsHandler implements HandlerInterface
|
|||||||
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
$file = $this->mediaFiles->episodeExists($parentShow->title, $command->season, $command->episode);
|
||||||
|
|
||||||
return new GetTvShowOptionsResult(
|
return new GetTvShowOptionsResult(
|
||||||
parentShow: $parentShow,
|
|
||||||
media: $media,
|
media: $media,
|
||||||
file: MediaFileDto::fromSplFileInfo($file),
|
file: MediaFileDto::fromSplFileInfo($file),
|
||||||
season: $command->season,
|
season: $command->season,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use OneToMany\RichBundle\Contract\ResultInterface;
|
|||||||
class GetTvShowOptionsResult implements ResultInterface
|
class GetTvShowOptionsResult implements ResultInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $parentShow,
|
|
||||||
public TmdbResult $media,
|
public TmdbResult $media,
|
||||||
public MediaFileDto|false $file,
|
public MediaFileDto|false $file,
|
||||||
public string $season,
|
public string $season,
|
||||||
|
|||||||
@@ -3,20 +3,14 @@
|
|||||||
namespace App\Twig\Components;
|
namespace App\Twig\Components;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\User\Database\CodecList;
|
|
||||||
use App\User\Database\QualityList;
|
use App\User\Database\QualityList;
|
||||||
use App\User\Database\ResolutionList;
|
|
||||||
use App\User\Dto\PreferenceOptions;
|
|
||||||
use App\User\Dto\PreferenceOptionsFactory;
|
|
||||||
use App\User\Dto\UserPreferencesFactory;
|
|
||||||
use App\User\Framework\Repository\PreferencesRepository;
|
use App\User\Framework\Repository\PreferencesRepository;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||||
|
|
||||||
#[AsLiveComponent]
|
#[AsLiveComponent]
|
||||||
final class Filter extends AbstractController
|
final class Filter
|
||||||
{
|
{
|
||||||
use DefaultActionTrait;
|
use DefaultActionTrait;
|
||||||
|
|
||||||
@@ -27,20 +21,15 @@ final class Filter extends AbstractController
|
|||||||
public array $reverseMappedQualities = [];
|
public array $reverseMappedQualities = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly PreferencesRepository $preferencesRepository,
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
) {
|
) {
|
||||||
$this->preferences = (array) PreferenceOptionsFactory::createSelectOptions();
|
$this->preferences = Map::from($this->preferencesRepository->findEnabled())
|
||||||
$this->userPreferences = (array) UserPreferencesFactory::createFromUser($security->getUser());
|
->rekey(fn($element) => $element->getId())
|
||||||
|
->map(fn($element) => $element->getPreferenceOptions()->toArray())
|
||||||
|
->toArray();
|
||||||
|
$this->userPreferences = Map::from($this->security->getUser()->getUserPreferenceValues())
|
||||||
|
->toArray();
|
||||||
$this->reverseMappedQualities = QualityList::getAsReverseMap();
|
$this->reverseMappedQualities = QualityList::getAsReverseMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getResolutionOptions()
|
|
||||||
{
|
|
||||||
return ResolutionList::asSelectOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCodecOptions()
|
|
||||||
{
|
|
||||||
return CodecList::asSelectOptions();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ use App\Search\Action\Command\GetMediaInfoCommand;
|
|||||||
use App\Search\Action\Handler\GetMediaInfoHandler;
|
use App\Search\Action\Handler\GetMediaInfoHandler;
|
||||||
use App\Search\TvEpisodePaginator;
|
use App\Search\TvEpisodePaginator;
|
||||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
|
||||||
use Symfony\UX\LiveComponent\Attribute\LiveArg;
|
|
||||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||||
|
|
||||||
#[AsLiveComponent]
|
#[AsLiveComponent]
|
||||||
final class TvEpisodeList
|
final class TvEpisodeList
|
||||||
{
|
{
|
||||||
use DefaultActionTrait;
|
use DefaultActionTrait;
|
||||||
use PaginateTrait;
|
use PaginateTrait;
|
||||||
@@ -29,12 +27,6 @@ final class TvEpisodeList
|
|||||||
#[LiveProp(writable: true)]
|
#[LiveProp(writable: true)]
|
||||||
public int $season = 1;
|
public int $season = 1;
|
||||||
|
|
||||||
#[LiveProp(writable: true)]
|
|
||||||
public int $reloadCount = 0;
|
|
||||||
|
|
||||||
#[LiveProp(writable: true)]
|
|
||||||
public ?int $episodeNumber = null;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private GetMediaInfoHandler $getMediaInfoHandler,
|
private GetMediaInfoHandler $getMediaInfoHandler,
|
||||||
) {}
|
) {}
|
||||||
@@ -42,14 +34,6 @@ final class TvEpisodeList
|
|||||||
public function getEpisodes()
|
public function getEpisodes()
|
||||||
{
|
{
|
||||||
$results = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand($this->imdbId, "tvshows", $this->season));
|
$results = $this->getMediaInfoHandler->handle(new GetMediaInfoCommand($this->imdbId, "tvshows", $this->season));
|
||||||
|
|
||||||
if (null !== $this->episodeNumber) {
|
|
||||||
$this->pageNumber = ceil($this->episodeNumber / $this->perPage);
|
|
||||||
$this->episodeNumber = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->reloadCount++;
|
|
||||||
|
|
||||||
return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage);
|
return new TvEpisodePaginator()->paginate($results, $this->pageNumber, $this->perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Twig\Dto;
|
|
||||||
|
|
||||||
class EpisodeIdDto
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $season,
|
|
||||||
public string $episode,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function asEpisodeId(): string
|
|
||||||
{
|
|
||||||
return "S". str_pad($this->season, 2, "0", STR_PAD_LEFT) .
|
|
||||||
"E". str_pad($this->episode, 2, "0", STR_PAD_LEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
if ("" !== $this->season && "" !== $this->episode) {
|
|
||||||
return $this->asEpisodeId();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ namespace App\Twig\Extensions;
|
|||||||
|
|
||||||
use App\Base\Service\MediaFiles;
|
use App\Base\Service\MediaFiles;
|
||||||
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
use App\Torrentio\Action\Result\GetTvShowOptionsResult;
|
||||||
use App\Twig\Dto\EpisodeIdDto;
|
|
||||||
use ChrisUllyott\FileSize;
|
use ChrisUllyott\FileSize;
|
||||||
use Twig\Attribute\AsTwigFilter;
|
use Twig\Attribute\AsTwigFilter;
|
||||||
use Twig\Attribute\AsTwigFunction;
|
use Twig\Attribute\AsTwigFunction;
|
||||||
@@ -64,42 +63,4 @@ class UtilExtension
|
|||||||
return "S". str_pad($season, 2, "0", STR_PAD_LEFT) .
|
return "S". str_pad($season, 2, "0", STR_PAD_LEFT) .
|
||||||
"E". str_pad($episode, 2, "0", STR_PAD_LEFT);
|
"E". str_pad($episode, 2, "0", STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[AsTwigFunction('episode_anchor')]
|
|
||||||
public function episodeAnchor($season, $episode): ?string
|
|
||||||
{
|
|
||||||
return "episode_" . (int) $season . "_" . (int) $episode;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[AsTwigFunction('extract_from_episode_id')]
|
|
||||||
public function extractFromEpisodeId(?string $episodeId): ?EpisodeIdDto
|
|
||||||
{
|
|
||||||
if (null === $episodeId) {
|
|
||||||
return new EpisodeIdDto("", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture season
|
|
||||||
$seasonMatch = [];
|
|
||||||
preg_match('/[sS]\d\d/', $episodeId, $seasonMatch);
|
|
||||||
if (empty($seasonMatch)) {
|
|
||||||
$season = "";
|
|
||||||
} else {
|
|
||||||
$season = str_replace(['S', 's'], '', $seasonMatch[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture episode
|
|
||||||
$episodeMatch = [];
|
|
||||||
preg_match('/[eE]\d\d/', $episodeId, $episodeMatch);
|
|
||||||
if (empty($episodeMatch)) {
|
|
||||||
$episode = "";
|
|
||||||
} else {
|
|
||||||
$episode = str_replace(['E', 'e'], '', $episodeMatch[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $season && null === $episode) {
|
|
||||||
return new EpisodeIdDto("", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EpisodeIdDto($season, $episode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\User\Action\Command;
|
namespace App\User\Action\Command;
|
||||||
|
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
|
||||||
|
|
||||||
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||||
class SaveUserMediaPreferencesCommand implements CommandInterface
|
class SaveUserMediaPreferencesCommand implements CommandInterface
|
||||||
@@ -15,15 +14,4 @@ class SaveUserMediaPreferencesCommand implements CommandInterface
|
|||||||
public string $language,
|
public string $language,
|
||||||
public string $provider,
|
public string $provider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function fromUserMediaPreferencesForm(FormInterface $form): self
|
|
||||||
{
|
|
||||||
return new static(
|
|
||||||
resolution: $form->get('resolution')->getData(),
|
|
||||||
codec: $form->get('codec')->getData(),
|
|
||||||
quality: $form->get('quality')->getData(),
|
|
||||||
language: $form->get('language')->getData(),
|
|
||||||
provider: $form->get('provider')->getData(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\User\Database;
|
|
||||||
|
|
||||||
class CodecList
|
|
||||||
{
|
|
||||||
public static $codecs = [
|
|
||||||
'h264',
|
|
||||||
'h265/HEVC',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function getCodecs()
|
|
||||||
{
|
|
||||||
return self::$codecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function asSelectOptions(): array
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
foreach (static::$codecs as $codec) {
|
|
||||||
$result[$codec] = $codec;
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -102,7 +102,7 @@ class QualityList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
$result = ['n/a' => null];
|
$result = [];
|
||||||
foreach (array_keys(static::$qualities) as $quality) {
|
foreach (array_keys(static::$qualities) as $quality) {
|
||||||
$result[$quality] = $quality;
|
$result[$quality] = $quality;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\User\Database;
|
|
||||||
|
|
||||||
class ResolutionList
|
|
||||||
{
|
|
||||||
public static $resolutions = [
|
|
||||||
'480p',
|
|
||||||
'720p',
|
|
||||||
'1080p',
|
|
||||||
'2160p',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function getResolutions()
|
|
||||||
{
|
|
||||||
return self::$resolutions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function asSelectOptions(): array
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
foreach (static::$resolutions as $resolution) {
|
|
||||||
$result[$resolution] = $resolution;
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\User\Dto;
|
|
||||||
|
|
||||||
class PreferenceOptions
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly array $resolutions,
|
|
||||||
public readonly array $codecs,
|
|
||||||
public readonly array $languages,
|
|
||||||
public readonly array $providers,
|
|
||||||
public readonly array $qualities,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\User\Dto;
|
|
||||||
|
|
||||||
use App\User\Database\CodecList;
|
|
||||||
use App\User\Database\CountryLanguages;
|
|
||||||
use App\User\Database\ProviderList;
|
|
||||||
use App\User\Database\QualityList;
|
|
||||||
use App\User\Database\ResolutionList;
|
|
||||||
|
|
||||||
class PreferenceOptionsFactory
|
|
||||||
{
|
|
||||||
public static function createSelectOptions(): PreferenceOptions
|
|
||||||
{
|
|
||||||
return new PreferenceOptions(
|
|
||||||
resolutions: ResolutionList::asSelectOptions(),
|
|
||||||
codecs: CodecList::asSelectOptions(),
|
|
||||||
languages: CountryLanguages::asSelectOptions(),
|
|
||||||
providers: ProviderList::asSelectOptions(),
|
|
||||||
qualities: QualityList::asSelectOptions(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,8 @@ class UserPreferencesFactory
|
|||||||
public static function createFromUser(UserInterface $user): UserPreferences
|
public static function createFromUser(UserInterface $user): UserPreferences
|
||||||
{
|
{
|
||||||
return new UserPreferences(
|
return new UserPreferences(
|
||||||
resolution: self::getValue($user, 'resolution'),
|
resolution: self::getNestedValue($user, 'resolution'),
|
||||||
codec: self::getValue($user, 'codec'),
|
codec: self::getNestedValue($user, 'codec'),
|
||||||
language: self::getValue($user, 'language'),
|
language: self::getValue($user, 'language'),
|
||||||
provider: self::getValue($user, 'provider'),
|
provider: self::getValue($user, 'provider'),
|
||||||
quality: self::getValue($user, 'quality'),
|
quality: self::getValue($user, 'quality'),
|
||||||
@@ -29,4 +29,19 @@ class UserPreferencesFactory
|
|||||||
}
|
}
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param User $user */
|
||||||
|
private static function getNestedValue(UserInterface $user, string $preferenceId): ?string
|
||||||
|
{
|
||||||
|
$preference = $user->getUserPreference($preferenceId);
|
||||||
|
if (null === $preference) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $preference->getPreference()
|
||||||
|
->getPreferenceOptions()
|
||||||
|
->filter(fn (PreferenceOption $option) => (string) $option->getId() === $preference->getPreferenceValue())
|
||||||
|
->first()
|
||||||
|
->getValue()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\User\Framework\Controller\Web;
|
namespace App\User\Framework\Controller\Web;
|
||||||
|
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
|
||||||
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
||||||
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||||
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
||||||
@@ -15,10 +14,8 @@ use App\User\Database\ProviderList;
|
|||||||
use App\User\Database\QualityList;
|
use App\User\Database\QualityList;
|
||||||
use App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use App\User\Framework\Form\GettingStartedFilterForm;
|
use App\User\Framework\Form\GettingStartedFilterForm;
|
||||||
use App\User\Framework\Form\UserMediaPreferencesForm;
|
|
||||||
use App\User\Framework\Repository\PreferencesRepository;
|
use App\User\Framework\Repository\PreferencesRepository;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
@@ -32,43 +29,53 @@ class PreferencesController extends AbstractController
|
|||||||
#[Route('/user/preferences', 'app_user_preferences', methods: ['GET'])]
|
#[Route('/user/preferences', 'app_user_preferences', methods: ['GET'])]
|
||||||
public function mediaPreferences(): Response
|
public function mediaPreferences(): Response
|
||||||
{
|
{
|
||||||
|
$mediaPreferences = $this->getUser()->getMediaPreferences();
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$languages = CountryLanguages::$languages;
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
sort($languages);
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
|
'preferences' => $this->preferencesRepository->findEnabled(),
|
||||||
|
'languages' => $languages,
|
||||||
|
'providers' => ProviderList::getProviders(),
|
||||||
|
'qualities' => QualityList::getBaseQualities(),
|
||||||
|
'mediaPreferences' => $mediaPreferences,
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
'preferences_form' => $form,
|
'filterForm' => $this->createForm(GettingStartedFilterForm::class, (array) UserPreferencesFactory::createFromUser($this->getUser())),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/user/preferences/media', 'app_user_media_preferences_submit', methods: ['POST'])]
|
#[Route('/user/preferences/media', 'app_save_media_preferences', methods: ['POST'])]
|
||||||
public function mediaPreferencesSubmit(
|
public function saveMediaPreferences(
|
||||||
Request $request,
|
SaveUserMediaPreferencesInput $input,
|
||||||
SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler
|
SaveUserMediaPreferencesHandler $saveUserMediaPreferencesHandler,
|
||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
|
$saveUserMediaPreferencesHandler->handle($input->toCommand());
|
||||||
|
$mediaPreferences = $this->getUser()->getMediaPreferences();
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$languages = CountryLanguages::$languages;
|
||||||
|
sort($languages);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
$this->broadcaster->alert(
|
||||||
$saveUserMediaPreferencesHandler->handle(
|
title: 'Success',
|
||||||
SaveUserMediaPreferencesCommand::fromUserMediaPreferencesForm($form)
|
message: 'Your media preferences have been saved.'
|
||||||
);
|
);
|
||||||
$this->broadcaster->alert('Success', 'Your media preferences have been saved.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
|
'preferences' => $this->preferencesRepository->findEnabled(),
|
||||||
|
'languages' => $languages,
|
||||||
|
'providers' => ProviderList::$providers,
|
||||||
|
'qualities' => QualityList::getBaseQualities(),
|
||||||
|
'mediaPreferences' => $mediaPreferences,
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
'preferences_form' => $form,
|
'filterForm' => $this->createForm(GettingStartedFilterForm::class ?? null),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,11 +86,11 @@ class PreferencesController extends AbstractController
|
|||||||
SaveUserDownloadPreferencesHandler $saveUserDownloadPreferencesHandler,
|
SaveUserDownloadPreferencesHandler $saveUserDownloadPreferencesHandler,
|
||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $saveUserDownloadPreferencesHandler->handle($input->toCommand())->downloadPreferences;
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$mediaPreferences = $this->getUser()->getMediaPreferences();
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
|
||||||
|
|
||||||
$saveUserDownloadPreferencesHandler->handle($input->toCommand());
|
$languages = CountryLanguages::$languages;
|
||||||
|
sort($languages);
|
||||||
|
|
||||||
$this->broadcaster->alert(
|
$this->broadcaster->alert(
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
@@ -93,8 +100,12 @@ class PreferencesController extends AbstractController
|
|||||||
return $this->render(
|
return $this->render(
|
||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
|
'preferences' => $this->preferencesRepository->findEnabled(),
|
||||||
|
'languages' => $languages,
|
||||||
|
'providers' => ProviderList::getProviders(),
|
||||||
|
'qualities' => QualityList::getBaseQualities(),
|
||||||
|
'mediaPreferences' => $mediaPreferences,
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
'preferences_form' => $form,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ class Preference
|
|||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?bool $enabled = null;
|
private ?bool $enabled = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, PreferenceOption>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: PreferenceOption::class, mappedBy: 'preference', fetch: 'EAGER')]
|
||||||
|
private Collection $preferenceOptions;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->preferenceOptions = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -83,4 +94,34 @@ class Preference
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, PreferenceOption>
|
||||||
|
*/
|
||||||
|
public function getPreferenceOptions(): Collection
|
||||||
|
{
|
||||||
|
return $this->preferenceOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPreferenceOption(PreferenceOption $preferenceOption): static
|
||||||
|
{
|
||||||
|
if (!$this->preferenceOptions->contains($preferenceOption)) {
|
||||||
|
$this->preferenceOptions->add($preferenceOption);
|
||||||
|
$preferenceOption->setPreference($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePreferenceOption(PreferenceOption $preferenceOption): static
|
||||||
|
{
|
||||||
|
if ($this->preferenceOptions->removeElement($preferenceOption)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($preferenceOption->getPreference() === $this) {
|
||||||
|
$preferenceOption->setPreference(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/User/Framework/Entity/PreferenceOption.php
Normal file
82
src/User/Framework/Entity/PreferenceOption.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Framework\Entity;
|
||||||
|
|
||||||
|
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: PreferenceOptionRepository::class)]
|
||||||
|
class PreferenceOption
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $value = null;
|
||||||
|
|
||||||
|
#[Ignore]
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'preferenceOptions')]
|
||||||
|
private ?Preference $preference = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?bool $enabled = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(?string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue(): ?string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(?string $value): static
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPreference(): ?Preference
|
||||||
|
{
|
||||||
|
return $this->preference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPreference(?Preference $preference): static
|
||||||
|
{
|
||||||
|
$this->preference = $preference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): ?bool
|
||||||
|
{
|
||||||
|
return $this->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEnabled(bool $enabled): static
|
||||||
|
{
|
||||||
|
$this->enabled = $enabled;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -215,6 +215,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
if (in_array($userPreference->getPreference()->getId(), ['language', 'provider', 'quality'])) {
|
if (in_array($userPreference->getPreference()->getId(), ['language', 'provider', 'quality'])) {
|
||||||
return $userPreference->getPreferenceValue();
|
return $userPreference->getPreferenceValue();
|
||||||
}
|
}
|
||||||
|
foreach ($userPreference->getPreference()->getPreferenceOptions() as $preferenceOption) {
|
||||||
|
if ($preferenceOption->getId() === (int) $userPreference->getPreferenceValue()) {
|
||||||
|
return $preferenceOption->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace App\User\Framework\Form;
|
namespace App\User\Framework\Form;
|
||||||
|
|
||||||
use App\User\Database\CodecList;
|
use Aimeos\Map;
|
||||||
use App\User\Database\CountryLanguages;
|
use App\User\Database\CountryLanguages;
|
||||||
use App\User\Database\ProviderList;
|
use App\User\Database\ProviderList;
|
||||||
use App\User\Database\QualityList;
|
use App\User\Database\QualityList;
|
||||||
use App\User\Database\ResolutionList;
|
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@@ -14,14 +14,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class GettingStartedFilterForm extends AbstractType
|
class GettingStartedFilterForm extends AbstractType
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PreferenceOptionRepository $preferenceOptionRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$this->addChoiceField($builder, 'language', CountryLanguages::asSelectOptions());
|
$this->addChoiceField($builder, 'language', CountryLanguages::asSelectOptions());
|
||||||
$this->addChoiceField($builder, 'quality', QualityList::asSelectOptions());
|
$this->addChoiceField($builder, 'quality', QualityList::asSelectOptions());
|
||||||
$this->addChoiceField($builder, 'provider', ProviderList::asSelectOptions());
|
$this->addChoiceField($builder, 'provider', ProviderList::asSelectOptions());
|
||||||
$this->addChoiceField($builder, 'resolution', ResolutionList::asSelectOptions());
|
$this->addChoiceField($builder, 'resolution', $this->getPreferenceChoices('resolution'));
|
||||||
$this->addChoiceField($builder, 'codec', CodecList::asSelectOptions());;
|
$this->addChoiceField($builder, 'codec', $this->getPreferenceChoices('codec'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
|
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
|
||||||
@@ -39,6 +42,16 @@ class GettingStartedFilterForm extends AbstractType
|
|||||||
$resolver->setDefaults([]);
|
$resolver->setDefaults([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getPreferenceChoices(string $preference): array
|
||||||
|
{
|
||||||
|
$options = $this->preferenceOptionRepository->findBy(['preference' => $preference]);
|
||||||
|
$result = [];
|
||||||
|
foreach ($options as $item) {
|
||||||
|
$result[$item->getName()] = $item->getId();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
private function addDefaultChoice(array $choices): iterable
|
private function addDefaultChoice(array $choices): iterable
|
||||||
{
|
{
|
||||||
return ['n/a' => null] + $choices;
|
return ['n/a' => null] + $choices;
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\User\Framework\Form;
|
|
||||||
|
|
||||||
use Aimeos\Map;
|
|
||||||
use App\User\Database\CodecList;
|
|
||||||
use App\User\Database\CountryLanguages;
|
|
||||||
use App\User\Database\ProviderList;
|
|
||||||
use App\User\Database\QualityList;
|
|
||||||
use App\User\Database\ResolutionList;
|
|
||||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGenerator;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
|
|
||||||
class UserMediaPreferencesForm extends AbstractType
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly UrlGeneratorInterface $urlGenerator,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
|
||||||
{
|
|
||||||
$this->addChoiceField($builder, 'language', CountryLanguages::asSelectOptions());
|
|
||||||
$this->addChoiceField($builder, 'quality', QualityList::asSelectOptions());
|
|
||||||
$this->addChoiceField($builder, 'provider', ProviderList::asSelectOptions());
|
|
||||||
$this->addChoiceField($builder, 'resolution', ResolutionList::asSelectOptions());
|
|
||||||
$this->addChoiceField($builder, 'codec', CodecList::asSelectOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
|
|
||||||
{
|
|
||||||
$question = [
|
|
||||||
'attr' => ['class' => 'w-64 text-input mb-4'],
|
|
||||||
'label_attr' => ['class' => 'w-64 text-white block font-semibold mb-2'],
|
|
||||||
'choices' => $this->addDefaultChoice($choices),
|
|
||||||
'required' => false,
|
|
||||||
];
|
|
||||||
$builder->add($fieldName, ChoiceType::class, $question);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
|
||||||
{
|
|
||||||
$resolver->setDefaults([
|
|
||||||
'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addDefaultChoice(array $choices): iterable
|
|
||||||
{
|
|
||||||
return ['n/a' => ''] + $choices;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
src/User/Framework/Repository/PreferenceOptionRepository.php
Normal file
43
src/User/Framework/Repository/PreferenceOptionRepository.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Framework\Repository;
|
||||||
|
|
||||||
|
use App\User\Framework\Entity\PreferenceOption;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<PreferenceOption>
|
||||||
|
*/
|
||||||
|
class PreferenceOptionRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PreferenceOption::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @return PreferenceOption[] Returns an array of PreferenceOption objects
|
||||||
|
// */
|
||||||
|
// public function findByExampleField($value): array
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('p')
|
||||||
|
// ->andWhere('p.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->orderBy('p.id', 'ASC')
|
||||||
|
// ->setMaxResults(10)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function findOneBySomeField($value): ?PreferenceOption
|
||||||
|
// {
|
||||||
|
// return $this->createQueryBuilder('p')
|
||||||
|
// ->andWhere('p.exampleField = :val')
|
||||||
|
// ->setParameter('val', $value)
|
||||||
|
// ->getQuery()
|
||||||
|
// ->getOneOrNullResult()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -10,8 +10,6 @@ module.exports = {
|
|||||||
"flex-row",
|
"flex-row",
|
||||||
"p-2",
|
"p-2",
|
||||||
"p-4",
|
"p-4",
|
||||||
"w-32",
|
|
||||||
"w-64",
|
|
||||||
"bg-blue-300",
|
"bg-blue-300",
|
||||||
"bg-orange-300",
|
"bg-orange-300",
|
||||||
"bg-fuchsia-300",
|
"bg-fuchsia-300",
|
||||||
@@ -22,7 +20,6 @@ module.exports = {
|
|||||||
"bg-rose-600",
|
"bg-rose-600",
|
||||||
"alert-success",
|
"alert-success",
|
||||||
"alert-warning",
|
"alert-warning",
|
||||||
"font-bold",
|
|
||||||
"min-w-64",
|
"min-w-64",
|
||||||
"rotate-180",
|
"rotate-180",
|
||||||
"-rotate-180",
|
"-rotate-180",
|
||||||
@@ -37,9 +34,6 @@ module.exports = {
|
|||||||
"rounded-sm",
|
"rounded-sm",
|
||||||
"rounded-md",
|
"rounded-md",
|
||||||
"r-tablecell",
|
"r-tablecell",
|
||||||
"animate__animated",
|
|
||||||
"animate__slideInLeft",
|
|
||||||
"animate__animateFaster"
|
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
@@ -14,11 +14,14 @@
|
|||||||
{% if entity.status != "Complete" %}
|
{% if entity.status != "Complete" %}
|
||||||
<turbo-stream action="update" target="download_progress_{{ id }}">
|
<turbo-stream action="update" target="download_progress_{{ id }}">
|
||||||
<template>
|
<template>
|
||||||
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
<div class="text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
||||||
style="width: {{ entity.progress }}%">
|
style="width:{{ entity.progress }}%">
|
||||||
|
</div>
|
||||||
|
<div class="absolute text-black text-center"
|
||||||
|
style="z-index: 400;margin-top: -1.25rem; margin-left: 1.2rem">
|
||||||
|
{{ entity.progress }}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="number text-black font-bold text-center z-40"
|
|
||||||
>{{ entity.progress }}%</div>
|
|
||||||
</template>
|
</template>
|
||||||
</turbo-stream>
|
</turbo-stream>
|
||||||
<turbo-stream action="update" target="action_buttons_{{ id }}">
|
<turbo-stream action="update" target="action_buttons_{{ id }}">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<li {{ attributes }} id="alert_{{ alert_id }}"
|
<li {{ attributes }} id="alert_{{ alert_id }}"
|
||||||
class="alert alert-{{ type|default('success') }}"
|
class="alert alert-{{ type|default('success') }}"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="shrink-0 w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="sr-only">Info</span>
|
<span class="sr-only">Info</span>
|
||||||
<h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3>
|
<h3 class="text-lg font-medium font-bold">{{ title|default('') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-sm w-[300px] font-bold overflow-hidden text-wrap">
|
<div class="mt-2 text-sm w-[350px] font-bold">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
<tr{{ attributes }} class="hover:bg-gray-200" id="ad_download_{{ download.id }}">
|
<tr{{ attributes }} class="hover:bg-gray-200" id="ad_download_{{ download.id }}">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-stone-800 truncate">
|
||||||
{% if download.mediaType == "movies" %}
|
<a href="{{ path('app_search_result', {imdbId: download.imdbId, mediaType: download.mediaType}) }}"
|
||||||
{% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType} %}
|
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate"
|
||||||
{% set route = path('app_search_result', routeParams) %}
|
>
|
||||||
{% else %}
|
|
||||||
{% set episodeIdDto = extract_from_episode_id(download.episodeId) %}
|
|
||||||
{% set routeParams = {imdbId: download.imdbId, mediaType: download.mediaType, season: episodeIdDto.season, episode: episodeIdDto.episode} %}
|
|
||||||
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ route }}"
|
|
||||||
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate">
|
|
||||||
{{ download.title }}
|
{{ download.title }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -26,23 +19,19 @@
|
|||||||
{{ download.mediaType }}
|
{{ download.mediaType }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="whitespace-nowrap gap-2 text-sm align-middle text-gray-800 dark:text-gray-50">
|
<td class="px-6 py-4 whitespace-nowrap text-sm align-middle text-gray-800 dark:text-gray-50">
|
||||||
{% if download.progress < 100 %}
|
{% if download.progress < 100 %}
|
||||||
<div class="flex flex-row items-center justify-center">
|
<div id="download_progress_{{ download.id }}" class="border-2 border-green-600 rounded-md text-center w-full h-6 align-middle overflow-hidden">
|
||||||
<div id="download_progress_{{ download.id }}" class="progress border-2 border-green-600 rounded-md text-center w-16 h-6 align-middle overflow-hidden">
|
<div class="text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
||||||
<div class="background text-black text-center rounded-sm text-bold bg-green-300 h-5 relative z-10"
|
style="width:{{ download.progress }}%">
|
||||||
style="width: {{ download.progress }}%">
|
|
||||||
</div>
|
|
||||||
<div class="number text-black font-bold text-center z-40"
|
|
||||||
>{{ download.progress }}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-black text-center" style="z-index: 400;margin-top: -1.25rem; margin-left: 1.2rem">{{ download.progress }}%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<twig:StatusBadge color="green" status="Complete" />
|
<twig:StatusBadge color="green" status="Complete" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td id="hidden md:table-cell action_buttons_{{ download.id }}" class="pl-2 pr-4 py-4 flex flex-row items-center justify-end">
|
<td id="hidden md:table-cell action_buttons_{{ download.id }}" class="px-6 py-4 flex flex-row items-center">
|
||||||
{% if download.status == 'In Progress' and download.progress < 100 %}
|
{% if download.status == 'In Progress' and download.progress < 100 %}
|
||||||
<button id="pause_{{ download.id }}" class="text-orange-500 hover:text-orange-600 mr-1 self-start" {{ stimulus_action('download_list', 'pauseDownload', 'click', {id: download.id}) }}>
|
<button id="pause_{{ download.id }}" class="text-orange-500 hover:text-orange-600 mr-1 self-start" {{ stimulus_action('download_list', 'pauseDownload', 'click', {id: download.id}) }}>
|
||||||
<twig:ux:icon name="icon-park-twotone:pause-one" width="16.75px" height="16.75px" class="rounded-full" />
|
<twig:ux:icon name="icon-park-twotone:pause-one" width="16.75px" height="16.75px" class="rounded-full" />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
data-result-filter-movie-results-outlet=".results"
|
data-result-filter-movie-results-outlet=".results"
|
||||||
data-result-filter-tv-results-outlet=".results"
|
data-result-filter-tv-results-outlet=".results"
|
||||||
data-result-filter-tv-episode-list-outlet=".episode-list"
|
data-result-filter-tv-episode-list-outlet=".episode-list"
|
||||||
data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason"
|
data-action="change->result-filter#filter movie-results:optionsLoaded@window->result-filter#loadOptions tv-results:optionsLoaded@window->result-filter#loadOptions action-button:downloadSeason@window->result-filter#downloadSeason"
|
||||||
>
|
>
|
||||||
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
||||||
<label for="resolution">
|
<label for="resolution">
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
value="{{ app.user.userPreferenceValues["resolution"] }}"
|
value="{{ app.user.userPreferenceValues["resolution"] }}"
|
||||||
>
|
>
|
||||||
<option value="">n/a</option>
|
<option value="">n/a</option>
|
||||||
{% for name, value in this.resolutionOptions %}
|
{% for option in this.preferences['resolution'] %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ option.value }}"
|
||||||
{{ value == this.userPreferences['resolution'] ? 'selected' }}
|
{{ option.value == this.userPreferences['resolution'] ? 'selected' }}
|
||||||
>{{ name }}</option>
|
>{{ option.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -26,10 +26,10 @@
|
|||||||
Codec
|
Codec
|
||||||
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
|
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
|
||||||
<option value="">n/a</option>
|
<option value="">n/a</option>
|
||||||
{% for name, value in this.codecOptions %}
|
{% for option in this.preferences['codec'] %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ option.value }}"
|
||||||
{{ value == this.userPreferences['codec'] ? 'selected' }}
|
{{ option.value == this.userPreferences['codec'] ? 'selected' }}
|
||||||
>{{ name }}</option>
|
>{{ option.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -73,15 +73,18 @@
|
|||||||
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
||||||
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
||||||
>
|
>
|
||||||
{% for season in range(1, results.media.episodes|length) %}
|
<option selected value="1">1</option>
|
||||||
<option value="{{ season }}"
|
{% for season in range(2, results.media.episodes|length) %}
|
||||||
{% if results.season == season %}
|
<option value="{{ season }}">{{ season }}</option>
|
||||||
selected="selected"
|
|
||||||
{% endif %}
|
|
||||||
>{{ season }}</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{# <label for="episodeNumber">#}
|
||||||
|
{# Episode#}
|
||||||
|
{# <select id="episodeNumber" name="episodeNumber" data-result-filter-target="episode" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-sm">#}
|
||||||
|
{# <option selected value="">n/a</option>#}
|
||||||
|
{# </select>#}
|
||||||
|
{# </label>#}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
|
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
|
||||||
class="loading-icon">
|
class="loading-icon">
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
<div class="flex flex-row gap-2 justify-end px-8">
|
<div class="flex flex-row gap-2 justify-end px-8">
|
||||||
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 border border-green-500 bg-green-800/60 rounded-ms text-sm font-semibold" show_cancel show_submit>
|
<twig:Modal heading="Back up a sec!" button_text="Download Season" submit_action="{{ stimulus_action('result_filter', 'downloadSeason', 'click')|stimulus_action('dialog', 'close') }}" button_class="px-1.5 py-1 bg-green-600 rounded-ms text-sm font-semibold" show_cancel show_submit>
|
||||||
Downloading an entire season this way will use the filter from your
|
Downloading an entire season this way will use the filter from your
|
||||||
<a href="{{ path('app_user_preferences') }}" class="text-underline">preferences</a> to choose
|
<a href="{{ path('app_user_preferences') }}" class="text-underline">preferences</a> to choose
|
||||||
the appropriate file(s).
|
the appropriate file(s).
|
||||||
@@ -99,7 +102,7 @@
|
|||||||
Do you wish to download <strong>season <span id="downloadSeasonModal">{{ results.season }}</span></strong> of "<strong>{{ results.media.title }}</strong>"?
|
Do you wish to download <strong>season <span id="downloadSeasonModal">{{ results.season }}</span></strong> of "<strong>{{ results.media.title }}</strong>"?
|
||||||
</twig:Modal>
|
</twig:Modal>
|
||||||
|
|
||||||
<button class="px-1.5 py-1 bg-green-800/60 hover:bg-green-700/60 border border-green-500 rounded-ms text-sm font-semibold"
|
<button class="px-1.5 py-1 bg-green-600 hover:bg-green-700 rounded-ms text-sm font-semibold"
|
||||||
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
||||||
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
||||||
>Download Selected</button>
|
>Download Selected</button>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="fixed z-40 top-4 right-3 md:top-10 md:right-10">
|
<div {{ turbo_stream_listen(app.session.get('mercure_alert_topic')) }} class="fixed z-40 top-10 right-10">
|
||||||
<div class="z-40">
|
<div class="z-40">
|
||||||
<ul id="alert_list" class="flex flex-col gap-2">
|
<ul id="alert_list" class="flex flex-col gap-2">
|
||||||
{% for message in app.flashes('warning') %}
|
{% for message in app.flashes('warning') %}
|
||||||
|
|||||||
@@ -3,19 +3,7 @@
|
|||||||
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
<a href="{{ path('app_search_result', {imdbId: monitor.imdbId, mediaType: monitor.monitorType|as_download_type}) }}"
|
||||||
class="mr-1 hover:underline rounded-md"
|
class="mr-1 hover:underline rounded-md"
|
||||||
>
|
>
|
||||||
|
{{ monitor.title }}
|
||||||
{% if monitor.monitorType == "movies" %}
|
|
||||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType} %}
|
|
||||||
{% set route = path('app_search_result', routeParams) %}
|
|
||||||
{% else %}
|
|
||||||
{% set episodeIdDto = extract_from_episode_id(monitor|monitor_media_id) %}
|
|
||||||
{% set routeParams = {imdbId: monitor.imdbId, mediaType: monitor.monitorType, season: episodeIdDto.season, episode: episodeIdDto.episode} %}
|
|
||||||
{% set route = path('app_search_result', routeParams) ~ "#" ~ episode_anchor(episodeIdDto.season, episodeIdDto.episode) %}
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ route }}"
|
|
||||||
class="mr-1 hover:underline rounded-md max-w-[10ch] md:max-w-[unset] truncate">
|
|
||||||
{{ monitor.title }}
|
|
||||||
</a>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<nav id="navbar" {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950 animate__animated animate__animateFaster">
|
<nav id="navbar" {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950 animate__animated animate__slideInLeft animate__slow">
|
||||||
<div class="px-4 py-4 flex flex-col gap-12">
|
<div class="px-4 py-4 flex flex-col gap-12">
|
||||||
<h1 class="text-3xl mt-12 md:mt-0 font-extrabold text-orange-500 mb-3"><a href="{{ path('app_index') }}">Torsearch</a></h1>
|
<h1 class="text-3xl mt-12 md:mt-0 font-extrabold text-orange-500 mb-3"><a href="{{ path('app_index') }}">Torsearch</a></h1>
|
||||||
<ul class="nav-list space-y-1">
|
<ul class="nav-list space-y-1">
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
class="episode-list flex flex-col gap-4"
|
class="episode-list flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
|
<div data-live-id="{{ uniqid() }}" class="episode-container flex flex-col gap-4">
|
||||||
{% for episode in this.getEpisodes().items %}
|
{% for episode in this.episodes.items %}
|
||||||
<episode-container id="{{ episode_anchor(episode['season_number'], episode['episode_number']) }}" class="results"
|
<div id="episode_{{ episode['season_number'] }}_{{ episode['episode_number'] }}" class="results"
|
||||||
show-title="{{ this.title }}"
|
|
||||||
data-tv-results-loading-icon-outlet=".loading-icon"
|
data-tv-results-loading-icon-outlet=".loading-icon"
|
||||||
data-download-button-outlet=".download-btn"
|
data-download-button-outlet=".download-btn"
|
||||||
{{ stimulus_controller('tv_results', {
|
{{ stimulus_controller('tv_results', {
|
||||||
@@ -16,73 +15,77 @@
|
|||||||
active: 'true',
|
active: 'true',
|
||||||
}) }}
|
}) }}
|
||||||
>
|
>
|
||||||
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
<div class="p-4 md:p-6 flex flex-col gap-6 bg-orange-500 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-60 rounded-md">
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
{% if episode['poster'] != null %}
|
{% if episode['poster'] != null %}
|
||||||
<img class="w-full md:w-64 rounded-lg" src="{{ episode['poster'] }}" />
|
<img class="w-full md:w-64 rounded-lg" src="{{ episode['poster'] }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
<div class="w-full md:w-64 min-w-64 sticky h-[144px] rounded-lg bg-gray-700 flex items-center justify-center">
|
||||||
<twig:ux:icon width="32" name="hugeicons:loading-01" />
|
<twig:ux:icon width="32" name="hugeicons:loading-01" />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex flex-col gap-4 grow">
|
<div class="flex flex-col gap-4 grow">
|
||||||
<h4 class="text-md font-bold">
|
<h4 class="text-md font-bold">
|
||||||
{{ episode['episode_number'] }}. {{ episode['name'] }}
|
{{ episode['episode_number'] }}. {{ episode['name'] }}
|
||||||
</h4>
|
</h4>
|
||||||
<p>{{ episode['overview']|truncate }}</p>
|
<p>{{ episode['overview']|truncate }}</p>
|
||||||
<div>
|
<div>
|
||||||
<button class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white" title="Click to expand the results table for season {{ episode['season_number'] }} episode {{ episode['episode_number'] }}.">
|
<button class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white"
|
||||||
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
{{ stimulus_action('tv-results', 'toggleList', 'click') }}
|
||||||
</button>
|
>
|
||||||
|
<span {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
||||||
|
</button>
|
||||||
|
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title='"{{ episode['name'] }}" aired on {{ episode['air_date']|date(null, 'UTC') }}.'>
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Air date {{ episode['name'] }}">
|
||||||
{{ episode['air_date']|date(null, 'UTC') }}
|
{{ episode['air_date']|date(null, 'UTC') }}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
<twig:Turbo:Frame id="meb_{{ this.imdbId }}_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('api.library.search', {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
season: episode['season_number'],
|
season: episode['season_number'],
|
||||||
episode: episode['episode_number'],
|
episode: episode['episode_number'],
|
||||||
block: 'media_exists_badge',
|
block: 'media_exists_badge',
|
||||||
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
target: "meb_" ~ this.imdbId ~"_" ~ episode_id(episode['season_number'], episode['episode_number'])
|
||||||
}) }}">
|
}) }}">
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-rose-600 rounded-lg text-white" title="Episode has not been downloaded yet.">
|
||||||
missing
|
missing
|
||||||
</small>
|
</small>
|
||||||
</twig:Turbo:Frame>
|
</twig:Turbo:Frame>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-4 justify-between">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<input class="episode-selector" type="checkbox"
|
|
||||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button class="dropdown-button flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90" title="Click to expand the results table for season {{ episode['season_number'] }} episode {{ episode['episode_number'] }}.">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
|
|
||||||
<path
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M20 6L10 16l10 10" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
<div class="flex flex-col gap-4 justify-between">
|
||||||
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
|
<div class="flex flex-col items-center">
|
||||||
tmdbId: this.tmdbId,
|
<input type="checkbox"
|
||||||
imdbId: this.imdbId,
|
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
||||||
season: episode['season_number'],
|
/>
|
||||||
episode: episode['episode_number'],
|
</div>
|
||||||
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
|
<button class="flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90"
|
||||||
block: 'tvshow_results'
|
{{ stimulus_target('tv-results', 'toggleButton') }}
|
||||||
}) }}" />
|
{{ stimulus_action('tv-results', 'toggleList', 'click') }}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20 6L10 16l10 10" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</episode-container>
|
<div class="inline-block overflow-hidden rounded-lg">
|
||||||
|
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
|
||||||
|
tmdbId: this.tmdbId,
|
||||||
|
imdbId: this.imdbId,
|
||||||
|
season: episode['season_number'],
|
||||||
|
episode: episode['episode_number'],
|
||||||
|
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
|
||||||
|
block: 'tvshow_results'
|
||||||
|
}) }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% set paginator = this.episodes %}
|
{% set paginator = this.episodes %}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<div{{ attributes }}>
|
|
||||||
<label class="text-gray-50" for="quality">{{ label }}</label>
|
|
||||||
<select class="p-1.5 rounded-md mb-2" name="quality" id="quality" value="{{ value }}">
|
|
||||||
{% if true == show_na %}
|
|
||||||
<option class="text-gray-800"
|
|
||||||
value=""
|
|
||||||
{{ value is null ? "selected" }}
|
|
||||||
>n/a</option>
|
|
||||||
{% endif %}
|
|
||||||
{% for option in options %}
|
|
||||||
<option class="text-gray-800"
|
|
||||||
value="{{ option.value }}"
|
|
||||||
{{ quality == option.value ? "selected" }}
|
|
||||||
>{{ option.label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
|
|
||||||
{% if "movies" == results.media.mediaType %}
|
{% if "movies" == results.media.mediaType %}
|
||||||
<div class="flex flex-row justify-start items-end grow">
|
<div class="flex flex-row justify-start items-end grow">
|
||||||
<span class="results-count-badge py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
<span class="py-1 px-1.5 mr-1 grow-0 font-bold text-xs bg-green-600 rounded-lg hover:cursor-pointer hover:bg-green-700 text-white">
|
||||||
<span class="results-count-number" id="movie_results_count">-</span> results
|
<span id="movie_results_count">-</span> results
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Release date {{ results.media.episodeAirDate }}">
|
<small class="py-1 px-1.5 mr-1 grow-0 font-bold bg-gray-700 rounded-lg font-normal text-white" title="Release date {{ results.media.episodeAirDate }}">
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<twig:Filter results="{{ results }}" filter="{{ filter }}" />
|
<twig:Filter results="{{ results }}" filter="{{ filter }}" />
|
||||||
|
|
||||||
{% if "movies" == results.media.mediaType %}
|
{% if "movies" == results.media.mediaType %}
|
||||||
<movie-container class="results"
|
<div class="results"
|
||||||
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
|
||||||
data-movie-results-loading-icon-outlet=".loading-icon"
|
data-movie-results-loading-icon-outlet=".loading-icon"
|
||||||
>
|
>
|
||||||
@@ -91,12 +91,12 @@
|
|||||||
target: 'movie_results_frame',
|
target: 'movie_results_frame',
|
||||||
block: 'movie_results'
|
block: 'movie_results'
|
||||||
}) }}" />
|
}) }}" />
|
||||||
</movie-container>
|
</div>
|
||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
results="results"
|
results="results"
|
||||||
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
||||||
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer" :episodeNumber="results.episode"
|
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer"
|
||||||
/>
|
/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<table class="w-full max-w-[75vw] text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 flex-row flex-no-wrap options-table"
|
<table class="w-full max-w-[75vw] text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 flex-row flex-no-wrap {{ results.media.mediaType == "tvshows" ? "hidden" : "options-table" }}"
|
||||||
{{ stimulus_target(controller, "list") }}
|
{{ stimulus_target(controller, "list") }}
|
||||||
>
|
>
|
||||||
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
|
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400">
|
||||||
@@ -41,29 +41,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="flex-1 sm:flex-none">
|
<tbody class="flex-1 sm:flex-none">
|
||||||
{% for result in results.results %}
|
{% for result in results.results %}
|
||||||
<tr is="dl-tr"
|
<tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow border-b border-gray-500" data-local-id="{{ result.localId }}" data-provider="{{ result.provider }}" data-quality="{{ result.quality }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}>
|
||||||
class="download-option bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow border-b border-gray-500"
|
|
||||||
url="{{ result.url }}"
|
|
||||||
size="{{ result.size }}"
|
|
||||||
quality="{{ result.quality }}"
|
|
||||||
resolution="{{ result.resolution }}"
|
|
||||||
codec="{{ result.codec }}"
|
|
||||||
seeders="{{ result.seeders }}"
|
|
||||||
provider="{{ result.provider }}"
|
|
||||||
languages="{{ result.languages|json_encode }}"
|
|
||||||
media-type="{{ results.media.mediaType }}"
|
|
||||||
imdb-id="{{ results.media.imdbId }}"
|
|
||||||
filename="{{ result.filename }}"
|
|
||||||
data-local-id="{{ result.localId }}"
|
|
||||||
{% if "tvshows" == results.media.mediaType %}
|
|
||||||
season="{{ result.season }}"
|
|
||||||
episode="{{ result.episodeNumber }}"
|
|
||||||
episode-id="{{ episode_id(result.season, result.episodeNumber) }}"
|
|
||||||
media-title="{{ results.parentShow.title }}"
|
|
||||||
{% else %}
|
|
||||||
media-title="{{ results.media.title }}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
<td id="size" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
<td id="size" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
|
||||||
{{ result.size }}
|
{{ result.size }}
|
||||||
</td>
|
</td>
|
||||||
@@ -86,7 +64,17 @@
|
|||||||
{{ result.languageFlags|raw }}
|
{{ result.languageFlags|raw }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-end text-gray-800 dark:text-gray-50 flex flex-row gap-2 items-center justify-start mb:justify-end">
|
||||||
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50">
|
<button class="download-btn p-1.5 bg-green-600 rounded-md text-gray-50"
|
||||||
|
{{ stimulus_controller('download_button', {
|
||||||
|
url: result.url,
|
||||||
|
title: results.media.title,
|
||||||
|
filename: result.filename,
|
||||||
|
mediaType: results.media.mediaType,
|
||||||
|
imdbId: results.media.imdbId ?? app.current_route_parameters.imdbId,
|
||||||
|
episodeId: results|episode_id_from_results
|
||||||
|
}) }}
|
||||||
|
{{ stimulus_action('download_button', 'download', 'click') }}
|
||||||
|
>
|
||||||
Download
|
Download
|
||||||
</button>
|
</button>
|
||||||
<label for="select">
|
<label for="select">
|
||||||
|
|||||||
@@ -5,19 +5,84 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col md:flex-row gap-2">
|
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||||
<twig:Card title="Media Preferences" class="w-full">
|
<twig:Card title="Media Preferences" class="w-full">
|
||||||
<p class="text-gray-50 mb-4">Define a filter to be pre-applied to your download options.</p>
|
<p class="text-gray-50 mb-2">Define a filter to be pre-applied to your download options.</p>
|
||||||
{{ form_start(preferences_form) }}
|
<form id="media_preferences" class="flex flex-col max-w-64" name="media_preferences" method="post" action="{{ path('app_save_media_preferences') }}">
|
||||||
{{ form_row(preferences_form.language) }}
|
<label class="text-gray-50" for="quality">Quality</label>
|
||||||
{{ form_row(preferences_form.quality) }}
|
<select class="p-1.5 rounded-md mb-2" name="quality" id="quality" value="{{ mediaPreferences['quality'].getPreferenceValue() }}">
|
||||||
{{ form_row(preferences_form.provider) }}
|
<option class="text-gray-800"
|
||||||
{{ form_row(preferences_form.resolution) }}
|
value=""
|
||||||
{{ form_row(preferences_form.codec) }}
|
{{ mediaPreferences['quality'].getPreferenceValue() is null ? "selected" }}
|
||||||
<button class="submit-button">Save</button>
|
>n/a</option>
|
||||||
{{ form_end(preferences_form) }}
|
{% for quality in qualities %}
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value="{{ quality }}"
|
||||||
|
{{ quality == mediaPreferences['quality'].getPreferenceValue() ? "selected" }}
|
||||||
|
>{{ quality }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="text-gray-50" for="resolution">Resolution</label>
|
||||||
|
<select class="p-1.5 rounded-md mb-2" name="resolution" id="resolution" value="{{ mediaPreferences['resolution'].getPreferenceValue() }}">
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value=""
|
||||||
|
{{ mediaPreferences['resolution'] is null ? "selected" }}
|
||||||
|
>n/a</option>
|
||||||
|
|
||||||
|
{% for pref in mediaPreferences['resolution'].getPreference().getPreferenceOptions() %}
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value="{{ pref.id }}"
|
||||||
|
{{ pref.id == mediaPreferences['resolution'].getPreferenceValue() ? "selected" }}
|
||||||
|
>{{ pref.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="text-gray-50" for="codec">Codec</label>
|
||||||
|
<select class="p-1.5 rounded-md mb-2" name="codec" id="codec" value="{{ mediaPreferences['codec'].getPreferenceValue() }}">
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value=""
|
||||||
|
{{ mediaPreferences['codec'].getPreferenceValue() is null ? "selected" }}
|
||||||
|
>n/a</option>
|
||||||
|
{% for pref in mediaPreferences['codec'].getPreference().getPreferenceOptions() %}
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value="{{ pref.id }}"
|
||||||
|
{{ pref.id == mediaPreferences['codec'].getPreferenceValue() ? "selected" }}
|
||||||
|
>{{ pref.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="text-gray-50" for="provider">Provider</label>
|
||||||
|
<select class="p-1.5 rounded-md mb-2" name="provider" id="provider" value="{{ mediaPreferences['provider'].getPreferenceValue() }}">
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value=""
|
||||||
|
{{ "" == mediaPreferences['provider'].getPreferenceValue() ? "selected" }}
|
||||||
|
>n/a</option>
|
||||||
|
{% for provider in providers %}
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value="{{ provider }}"
|
||||||
|
{{ provider == mediaPreferences['provider'].getPreferenceValue() ? "selected" }}
|
||||||
|
>{{ provider }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="text-gray-50" for="language">Language</label>
|
||||||
|
<select class="p-1.5 rounded-md mb-2" name="language" id="language" value="{{ mediaPreferences['language'].getPreferenceValue() }}">
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value=""
|
||||||
|
{{ mediaPreferences['language'].getPreferenceValue() is null ? "selected" }}
|
||||||
|
>n/a</option>
|
||||||
|
{% for language in languages %}
|
||||||
|
<option class="text-gray-800"
|
||||||
|
value="{{ language }}"
|
||||||
|
{{ language == mediaPreferences['language'].getPreferenceValue() ? "selected" }}
|
||||||
|
>{{ language }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button class="px-1.5 py-1 max-w-20 rounded-md bg-green-600 text-white" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
|
|
||||||
<twig:Card title="Download Preferences" class="w-full">
|
<twig:Card title="Download Preferences" class="w-full">
|
||||||
<p class="text-gray-50 mb-4">Change how your downloads are stored.</p>
|
<p class="text-gray-50 mb-2">Change how your downloads are stored.</p>
|
||||||
<form id="download_preferences" class="flex flex-col" name="download_preferences" method="post" action="{{ path('app_save_download_preferences') }}">
|
<form id="download_preferences" class="flex flex-col" name="download_preferences" method="post" action="{{ path('app_save_download_preferences') }}">
|
||||||
<div class="flex flex-row gap-2 mb-2">
|
<div class="flex flex-row gap-2 mb-2">
|
||||||
<input type="hidden" name="movie_folder" id="movie_folder_hidden" value="0" />
|
<input type="hidden" name="movie_folder" id="movie_folder_hidden" value="0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user