Compare commits
18 Commits
dev-user-p
...
v0.28.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 0430dba6a9 | |||
| beed7d6940 | |||
| 924472ed56 | |||
| 7dd61355b7 | |||
| 2a1f69edd4 | |||
| 9db0bfd4c6 | |||
| 18a165fc40 | |||
| 0e13b74b3b | |||
| f9ec089f8b | |||
| 87e72ec55e | |||
| 23a88ec6bb | |||
| d33a961f2d | |||
| 566886ef0e | |||
| 65acd5d21b | |||
| a27fcf334a | |||
| 56c5156380 | |||
| 18b00fc5ae | |||
| e39faa3398 |
9
assets/bootstrap.js
vendored
9
assets/bootstrap.js
vendored
@@ -1,9 +1,12 @@
|
|||||||
|
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';
|
import 'animate.css';
|
||||||
import Brock from './components/brock.js';
|
|
||||||
|
|
||||||
const app = startStimulusApp();
|
const app = startStimulusApp();
|
||||||
// register any custom, 3rd party controllers here
|
// register any custom, 3rd party controllers here
|
||||||
@@ -11,4 +14,6 @@ app.register('popover', Popover);
|
|||||||
app.register('dialog', Dialog);
|
app.register('dialog', Dialog);
|
||||||
app.register('dropdown', Dropdown);
|
app.register('dropdown', Dropdown);
|
||||||
|
|
||||||
customElements.define('brock-app', Brock);
|
customElements.define('episode-container', EpisodeContainer);
|
||||||
|
customElements.define('movie-container', MovieContainer);
|
||||||
|
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
export default class Brock extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
connectedCallback() {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.innerHTML = `
|
|
||||||
Yo, yo, yo! Waddup ${this.name}, doe, it's Brocky fresh!
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attribute change
|
|
||||||
attributeChangedCallback(property, oldValue, newValue) {
|
|
||||||
if (oldValue === newValue) return;
|
|
||||||
this[ property ] = newValue;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observedAttributes() {
|
|
||||||
return ['name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
187
assets/components/download-option-tr.js
Normal file
187
assets/components/download-option-tr.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
export default class DownloadOptionTr extends HTMLTableRowElement {
|
||||||
|
H264_CODECS = {
|
||||||
|
'h264': 'h264',
|
||||||
|
'h.264': 'h264',
|
||||||
|
'x264': 'h264',
|
||||||
|
}
|
||||||
|
H265_CODECS = {
|
||||||
|
'h265': 'h265',
|
||||||
|
'h.265': 'h265',
|
||||||
|
'x265': 'h265',
|
||||||
|
'hevc': 'h265',
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadBtnEl;
|
||||||
|
#selectEpisodeInputEl;
|
||||||
|
|
||||||
|
url;
|
||||||
|
size;
|
||||||
|
quality;
|
||||||
|
resolution;
|
||||||
|
codec;
|
||||||
|
seeders;
|
||||||
|
provider;
|
||||||
|
languages;
|
||||||
|
mediaType;
|
||||||
|
season;
|
||||||
|
episode;
|
||||||
|
filename;
|
||||||
|
imdbId;
|
||||||
|
episodeId;
|
||||||
|
mediaTitle;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.url = this.getAttribute('url');
|
||||||
|
this.size = this.getAttribute('size');
|
||||||
|
this.quality = this.getAttribute('quality');
|
||||||
|
this.resolution = this.getAttribute('resolution');
|
||||||
|
this.codec = this.getAttribute('codec');
|
||||||
|
this.seeders = this.getAttribute('seeders');
|
||||||
|
this.provider = this.getAttribute('provider');
|
||||||
|
this.filename = this.getAttribute('filename');
|
||||||
|
this.imdbId = this.getAttribute('imdb-id');
|
||||||
|
this.languages = JSON.parse(this.getAttribute('languages'));
|
||||||
|
this.mediaType = this.getAttribute('media-type');
|
||||||
|
this.mediaTitle = this.getAttribute('media-title');
|
||||||
|
this.season = this.getAttribute('season') ?? null;
|
||||||
|
this.episode = this.getAttribute('episode') ?? null;
|
||||||
|
this.episodeId = this.getAttribute('episode-id') ?? null;
|
||||||
|
this.#downloadBtnEl = this.querySelector('.download-btn');
|
||||||
|
this.#selectEpisodeInputEl = this.querySelector('input[type="checkbox"]');
|
||||||
|
|
||||||
|
this.#downloadBtnEl.addEventListener('click', () => this.download());
|
||||||
|
}
|
||||||
|
get isSelected() {
|
||||||
|
return this.#selectEpisodeInputEl.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isSelected(value) {
|
||||||
|
this.#selectEpisodeInputEl.checked = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter({ detail: { activeFilter } }) {
|
||||||
|
const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`)
|
||||||
|
|
||||||
|
let include = true;
|
||||||
|
this.classList.add('r-tablerow');
|
||||||
|
this.classList.remove('hidden');
|
||||||
|
optionHeader.classList.add('r-tablerow');
|
||||||
|
optionHeader.classList.remove('hidden');
|
||||||
|
|
||||||
|
this.querySelector('input[type="checkbox"]').checked = false;
|
||||||
|
|
||||||
|
if (!this.#validateResolutions(activeFilter.resolution)) {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#validateCodecs(activeFilter.codec)) {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#validateLanguages(activeFilter.language)) {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#validateQualities(activeFilter.quality)) {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#validateProviders(activeFilter.provider)) {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === include) {
|
||||||
|
this.classList.remove('r-tablerow');
|
||||||
|
this.classList.add('hidden');
|
||||||
|
optionHeader.classList.remove('r-tablerow');
|
||||||
|
optionHeader.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return include;
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
fetch('/api/download', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: this.url,
|
||||||
|
title: this.mediaTitle,
|
||||||
|
filename: this.filename,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
imdbId: this.imdbId,
|
||||||
|
episodeId: this.episodeId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
console.log(json)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateResolutions(selectedOptions) {
|
||||||
|
return this.#validateIntersection(selectedOptions, this.resolution.trim().split(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateCodecs(selectedOptions) {
|
||||||
|
if (this.#validateIntersection(selectedOptions, Object.keys(this.H264_CODECS))) {
|
||||||
|
return this.#validateIntersection(
|
||||||
|
selectedOptions,
|
||||||
|
[...this.codec.trim().split(','), '', 'n/a']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.#validateIntersection(selectedOptions, Object.keys(this.H265_CODECS))) {
|
||||||
|
return this.#validateIntersection(
|
||||||
|
selectedOptions,
|
||||||
|
[...this.codec.trim().split(','), '', 'n/a']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateQualities(selectedOptions) {
|
||||||
|
return this.#validateIntersection(selectedOptions, this.quality.trim().split(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateProviders(selectedOptions) {
|
||||||
|
return this.#validateIntersection(selectedOptions, this.provider.trim().split(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateLanguages(selectedOptions) {
|
||||||
|
return this.#validateIntersection(selectedOptions, this.languages);
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateIntersection(selectedOptions, localOptions) {
|
||||||
|
if (selectedOptions === null || selectedOptions === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof selectedOptions === 'string' || selectedOptions instanceof String) {
|
||||||
|
selectedOptions = [selectedOptions];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedOptions.length === 0 ||
|
||||||
|
(selectedOptions.length === 1 && selectedOptions[0] === "") ||
|
||||||
|
(selectedOptions.length === 1 && selectedOptions[0] === "n/a")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#doesIntersect(localOptions, selectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#doesIntersect(a, b) {
|
||||||
|
if (a.length === 0 || b.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.#intersect(a, b).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#intersect(a, b) {
|
||||||
|
return a.filter(Set.prototype.has, new Set(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
76
assets/components/episode-container.js
Normal file
76
assets/components/episode-container.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
assets/components/movie-container.js
Normal file
36
assets/components/movie-container.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,22 @@ export default class extends Controller {
|
|||||||
static targets = ['icon']
|
static targets = ['icon']
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
this.element.hideIcon = this.hideIcon.bind(this);
|
||||||
|
this.element.showIcon = this.showIcon.bind(this);
|
||||||
|
this.element.toggleIcon = this.toggleIcon.bind(this);
|
||||||
|
this.element.isVisibile = this.isVisible.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible() {
|
||||||
|
return !this.iconTarget.classList.contains('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
showIcon() {
|
||||||
|
this.iconTarget.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideIcon() {
|
||||||
|
this.iconTarget.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleIcon() {
|
toggleIcon() {
|
||||||
@@ -26,6 +41,8 @@ export default class extends Controller {
|
|||||||
if (this.countValue === this.totalValue) {
|
if (this.countValue === this.totalValue) {
|
||||||
this.toggleIcon();
|
this.toggleIcon();
|
||||||
this.countValue = 0;
|
this.countValue = 0;
|
||||||
|
console.log('filtering')
|
||||||
|
document.getElementById('filter').filterResults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ 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,
|
||||||
@@ -30,73 +27,8 @@ 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.dispatch('optionsLoaded', {detail: {options: this.options}})
|
|
||||||
this.loadingIconOutlet.toggleIcon();
|
|
||||||
this.resultCountEl.innerText = this.options.length;
|
this.resultCountEl.innerText = this.options.length;
|
||||||
}
|
this.loadingIconOutlet.toggleIcon();
|
||||||
|
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,23 @@ 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 = []
|
||||||
seasons = []
|
seasons = []
|
||||||
|
|
||||||
activeFilter = {
|
activeFilter = {
|
||||||
"resolution": "",
|
"resolution": [],
|
||||||
"codec": "",
|
"codec": [],
|
||||||
"language": "",
|
"language": [],
|
||||||
"provider": "",
|
"provider": [],
|
||||||
"quality": "",
|
"quality": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
static outlets = ['movie-results', 'tv-results', 'tv-episode-list']
|
defaultOptions = '<option value="-">-</option>';
|
||||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
|
||||||
|
static outlets = ['tv-episode-list']
|
||||||
|
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected']
|
||||||
static values = {
|
static values = {
|
||||||
'imdbId': String,
|
'imdbId': String,
|
||||||
'media-type': String,
|
'media-type': String,
|
||||||
@@ -32,133 +31,77 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
|
await this.setInitialFilter();
|
||||||
|
this.setTimerToStopLoadingIcon();
|
||||||
|
this.element.filterResults = this.filter.bind(this);
|
||||||
|
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInitialFilter() {
|
||||||
|
const response = await fetch('/api/user/filters');
|
||||||
|
const filters = await response.json();
|
||||||
|
if (filters.length > 0) {
|
||||||
|
this.activeFilter = filters[0];
|
||||||
|
}
|
||||||
if (this.mediaTypeValue === "tvshows") {
|
if (this.mediaTypeValue === "tvshows") {
|
||||||
this.activeFilter['season'] = 1;
|
this.activeFilter['season'] = 1;
|
||||||
}
|
}
|
||||||
await this.filter();
|
}
|
||||||
|
|
||||||
|
setTimerToStopLoadingIcon() {
|
||||||
|
setTimeout(() => this.loadingIconTarget.hideIcon(), 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, option.dataset);
|
option.filter({detail: {activeFilter: this.activeFilter }});
|
||||||
this.addProviders(option, option.dataset);
|
|
||||||
this.addQualities(option, option.dataset);
|
|
||||||
})
|
})
|
||||||
await this.filter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addLanguages(option, props) {
|
selectAllEpisodes() {
|
||||||
const languages = Object.assign([], JSON.parse(props['languages']));
|
document.dispatchEvent(new CustomEvent('selectEpisodeForDownload', {
|
||||||
languages.forEach((language) => {
|
detail: {
|
||||||
if (!this.languages.includes(language)) {
|
select: this.selectAllTarget.checked,
|
||||||
this.languages.push(language);
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
const preferred = this.languageTarget.dataset.preferred;
|
|
||||||
if (preferred) {
|
|
||||||
this.languageTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
|
|
||||||
this.languageTarget.innerHTML += '<option value="">n/a</option>';
|
|
||||||
} else {
|
|
||||||
this.languageTarget.innerHTML = '<option value="">n/a</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.languageTarget.innerHTML += this.languages.sort()
|
|
||||||
.map((language) => {
|
|
||||||
const preferred = this.languageTarget.dataset.preferred;
|
|
||||||
if (preferred === language) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return '<option value="'+language+'">'+language+'</option>';
|
|
||||||
})
|
|
||||||
.join();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addProviders(option, props) {
|
downloadSelectedEpisodes() {
|
||||||
if (!this.providers.includes(props['provider'])) {
|
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
|
||||||
this.providers.push(props['provider']);
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferred = this.providerTarget.dataset.preferred;
|
|
||||||
if (preferred) {
|
|
||||||
this.providerTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
|
|
||||||
this.providerTarget.innerHTML += '<option value="">n/a</option>';
|
|
||||||
} else {
|
|
||||||
this.providerTarget.innerHTML = '<option value="">n/a</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.providerTarget.innerHTML += this.providers.sort()
|
|
||||||
.map((provider) => {
|
|
||||||
const preferred = this.languageTarget.dataset.preferred;
|
|
||||||
if (preferred === provider) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return '<option value="' + provider + '">' + provider + '</option>'
|
|
||||||
})
|
|
||||||
.join();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addQualities(option, props) {
|
filter() {
|
||||||
if (!this.qualities.includes(props['quality'])) {
|
|
||||||
if (props['quality'].toLowerCase() in this.reverseMappedQualitiesValue) {
|
|
||||||
this.qualities.push(props['quality']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferred = this.qualityTarget.dataset.preferred;
|
|
||||||
if (preferred) {
|
|
||||||
this.qualityTarget.innerHTML = '<option value="'+preferred+'" selected>'+preferred+'</option>';
|
|
||||||
this.qualityTarget.innerHTML += '<option value="">n/a</option>';
|
|
||||||
} else {
|
|
||||||
this.qualityTarget.innerHTML = '<option value="">n/a</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.qualityTarget.innerHTML += this.qualities.sort()
|
|
||||||
.map((quality) => {
|
|
||||||
const preferred = this.qualityTarget.dataset.preferred;
|
|
||||||
if (preferred === quality) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return '<option value="' + quality + '">' + quality + '</option>'
|
|
||||||
})
|
|
||||||
.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.#fetchValuesFromNodeList(this.resolutionTarget.selectedOptions),
|
||||||
"codec": this.codecTarget.value,
|
"codec": this.#fetchValuesFromNodeList(this.codecTarget.selectedOptions),
|
||||||
"language": this.languageTarget.value,
|
"language": this.#fetchValuesFromNodeList(this.languageTarget.selectedOptions),
|
||||||
"provider": this.providerTarget.value,
|
"provider": this.#fetchValuesFromNodeList(this.providerTarget.selectedOptions),
|
||||||
"quality": this.qualityTarget.value,
|
"quality": this.#fetchValuesFromNodeList(this.qualityTarget.selectedOptions),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("movies" === this.mediaTypeValue) {
|
if ("tvshows" === this.mediaTypeValue) {
|
||||||
results = this.movieResultsOutlets;
|
downloadSeasonSpan.innerText = this.seasonTarget.value;
|
||||||
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: {
|
||||||
@@ -167,20 +110,15 @@ export default class extends Controller {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAllEpisodes() {
|
#fetchValuesFromNodeList(nodeList) {
|
||||||
this.tvResultsOutlets.forEach((episode) => {
|
return [...nodeList].map(option => option.value)
|
||||||
if (episode.isActive()) {
|
|
||||||
episode.selectEpisodeForDownload()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSelectedEpisodes() {
|
#serializeSelectOptions(options) {
|
||||||
this.tvResultsOutlets.forEach(episode => {
|
return this.defaultOptions + options.sort()
|
||||||
if (episode.isActive() && episode.isSelected()) {
|
.map((option) => {
|
||||||
episode.download();
|
return '<option value="' + option + '">' + option + '</option>'
|
||||||
}
|
})
|
||||||
});
|
.join();
|
||||||
this.selectAllTarget.checked = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export default class extends Controller {
|
|||||||
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
|
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
|
||||||
window.location.href = `/search?term=${autocompleteController.tomSelect.lastValue}`
|
window.location.href = `/search?term=${autocompleteController.tomSelect.lastValue}`
|
||||||
}
|
}
|
||||||
|
document.querySelector("#search-button").addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
|
||||||
|
window.location.href = `/search?term=${autocompleteController.tomSelect.lastQuery}`
|
||||||
|
});
|
||||||
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect);
|
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect);
|
||||||
this.element.addEventListener('autocomplete:connect', this._onConnect);
|
this.element.addEventListener('autocomplete:connect', this._onConnect);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ export default class extends Controller {
|
|||||||
});
|
});
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
let targetElement = document.querySelector(window.location.hash);
|
let targetElement = document.querySelector(window.location.hash);
|
||||||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
if (targetElement) {
|
||||||
targetElement.classList.add('animate__animated', 'animate__pulse', 'animate__faster');
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
targetElement.classList.add('animate__animated', 'animate__pulse', 'animate__faster');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,149 +18,23 @@ export default class extends Controller {
|
|||||||
active: Boolean,
|
active: Boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
static targets = ['list', 'count', 'episodeSelector', 'toggleButton', 'listContainer']
|
static targets = ['list', 'count', 'episodeSelector',]
|
||||||
static outlets = ['loading-icon']
|
static outlets = ['loading-icon']
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
optionsLoaded = false
|
|
||||||
isOpen = false
|
|
||||||
|
|
||||||
async listTargetConnected() {
|
listTargetConnected() {
|
||||||
this.options = this.element.querySelectorAll('tbody tr');
|
this.element.options = this.element.querySelectorAll('tbody tr');
|
||||||
if (this.options.length > 0) {
|
if (this.element.options.length > 0) {
|
||||||
this.options.forEach((option) =>
|
this.element.options.forEach((option) =>
|
||||||
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
option.querySelector('.download-btn').dataset['title'] = this.titleValue
|
||||||
);
|
);
|
||||||
this.options[0].querySelector('input[type="checkbox"]').checked = true;
|
this.element.options[0].querySelector('input[type="checkbox"]').checked = true;
|
||||||
this.dispatch('optionsLoaded', {detail: {options: this.options}})
|
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}}));
|
||||||
this.loadingIconOutlet.increaseCount();
|
|
||||||
} else {
|
} else {
|
||||||
this.countTarget.innerText = 0;
|
this.countTarget.innerText = 0;
|
||||||
this.episodeSelectorTarget.disabled = true;
|
this.episodeSelectorTarget.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
this.loadingIconOutlet.increaseCount();
|
||||||
|
|
||||||
//
|
|
||||||
// async clearCache() {
|
|
||||||
// await fetch(`/torrentio/tvshows/clear/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
|
|
||||||
// .then(res => res.text())
|
|
||||||
// .then(response => {});
|
|
||||||
// }
|
|
||||||
|
|
||||||
async setActive() {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
assets/icons/zondicons/checkmark.svg
Normal file
1
assets/icons/zondicons/checkmark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="m0 11l2-2l5 5L18 3l2 2L7 18z"/></svg>
|
||||||
|
After Width: | Height: | Size: 127 B |
@@ -68,6 +68,16 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
@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 px-2 py-1 bg-transparent border-b-2 border-orange-400
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-input[multiple="multiple"] {
|
||||||
|
@apply bg-transparent backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input option[checked="checked"],
|
||||||
|
.text-input option[checked],
|
||||||
|
.text-input option[selected] {
|
||||||
|
@apply bg-orange-500/60
|
||||||
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
@apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40
|
@apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40
|
||||||
}
|
}
|
||||||
@@ -130,3 +140,52 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter {
|
||||||
|
.ts-wrapper {
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.ts-control {
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
@apply bg-orange-500/60 backdrop-filter backdrop-blur-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item[data-ts-item] {
|
||||||
|
background-image: none !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
@apply bg-orange-500 rounded-ms font-bold text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@apply border border-orange-500 bg-transparent rounded-ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {
|
||||||
|
@apply border-l border-l-orange-600 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
@apply flex flex-col gap-1 justify-between;
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
"@auto-scripts"
|
"@auto-scripts"
|
||||||
],
|
],
|
||||||
|
"tail": "docker compose exec app ./bin/console tailwind:build --watch",
|
||||||
"sym": "docker compose exec app ./bin/console"
|
"sym": "docker compose exec app ./bin/console"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Base\Framework\Controller;
|
namespace App\Base\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Monitor\Action\Command\MonitorTvShowCommand;
|
||||||
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
use App\Monitor\Action\Handler\MonitorTvShowHandler;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
@@ -48,4 +49,13 @@ final class IndexController extends AbstractController
|
|||||||
'message' => 'Email sent!'
|
'message' => 'Email sent!'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/test')]
|
||||||
|
public function monitorTvShow(): Response
|
||||||
|
{
|
||||||
|
$this->monitorTvShowHandler->handle(new MonitorTvShowCommand(96));
|
||||||
|
return $this->json([
|
||||||
|
'Success' => 'Monitor added'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace App\Base\Service;
|
|||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Download\Framework\Entity\Download;
|
use App\Download\Framework\Entity\Download;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|||||||
11
src/Base/Util/ImdbMatcher.php
Normal file
11
src/Base/Util/ImdbMatcher.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Base\Util;
|
||||||
|
|
||||||
|
class ImdbMatcher
|
||||||
|
{
|
||||||
|
public static function isMatch(string $imdbId): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^tt\d{7}$/', $imdbId);
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/Base/Util/PTN.php
Normal file
246
src/Base/Util/PTN.php
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Parse Torrent Name (PTN)
|
||||||
|
*
|
||||||
|
* PHP port of parse-torrent-name written in Python.
|
||||||
|
*
|
||||||
|
* Javascript version by jzjzjzj
|
||||||
|
* https://github.com/jzjzjzj/parse-torrent-name
|
||||||
|
*
|
||||||
|
* Python version by divijbindlish
|
||||||
|
* https://github.com/divijbindlish/parse-torrent-name
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014 - 2018, British Columbia Institute of Technology
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @package PTN
|
||||||
|
* @author Drew Smith
|
||||||
|
* @copyright copyright (c) 2018, Nihilarr (https://www.nihilarr.com)
|
||||||
|
* @license http://opensource.org/licenses/MIT MIT License
|
||||||
|
* @link https://gitlab.com/nihilarr/parse-torrent-name
|
||||||
|
* @version 0.0.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Base\Util;
|
||||||
|
|
||||||
|
class PTN {
|
||||||
|
|
||||||
|
public $torrent;
|
||||||
|
public $excess_raw;
|
||||||
|
public $group_raw;
|
||||||
|
public $start;
|
||||||
|
public $end;
|
||||||
|
public $title_raw;
|
||||||
|
public $parts;
|
||||||
|
|
||||||
|
public $patterns = array(
|
||||||
|
array('season' => '(s?([0-9]{1,3}))[ex]'),
|
||||||
|
array('episode' => '([ex]([0-9]{1,3})(?:[^0-9]|$))'),
|
||||||
|
array('year' => '([\[\(]?((?:19[0-9]|20[01])[0-9])[\]\)]?)'),
|
||||||
|
array('resolution' => '([0-9]{3,4}p)'),
|
||||||
|
array('quality' => '((?:PPV\.)?[HP]DTV|(?:HD)?CAM|B[DR]Rip|(?:HD-?)?TS|(?:PPV )?WEB-?DL(?: DVDRip)?|HDRip|DVDRip|DVDRIP|CamRip|W[EB]BRip|BluRay|DvDScr|hdtv|telesync)'),
|
||||||
|
array('codec' => '(xvid|[hx]\.?26[45])'),
|
||||||
|
array('audio' => '(MP3|DD5\.?1|Dual[\- ]Audio|LiNE|DTS|AAC[.-]LC|AAC(?:\.?2\.0)?|AC3(?:\.5\.1)?)'),
|
||||||
|
array('group' => '(- ?([^-]+(?:-={[^-]+-?$)?))$'),
|
||||||
|
array('region' => 'R[0-9]'),
|
||||||
|
array('extended' => '(EXTENDED(:?.CUT)?)'),
|
||||||
|
array('hardcoded' => 'HC'),
|
||||||
|
array('proper' => 'PROPER'),
|
||||||
|
array('repack' => 'REPACK'),
|
||||||
|
array('container' => '(MKV|AVI|MP4)'),
|
||||||
|
array('widescreen' => 'WS'),
|
||||||
|
array('website' => '^(\[ ?([^\]]+?) ?\])'),
|
||||||
|
array('language' => '(rus\.eng|ita\.eng)'),
|
||||||
|
array('sbs' => '(?:Half-)?SBS'),
|
||||||
|
array('unrated' => 'UNRATED'),
|
||||||
|
array('size' => '(\d+(?:\.\d+)?(?:GB|MB))'),
|
||||||
|
array('3d' => '3D')
|
||||||
|
);
|
||||||
|
|
||||||
|
public $types = array(
|
||||||
|
'season' => 'integer',
|
||||||
|
'episode' => 'integer',
|
||||||
|
'year' => 'integer',
|
||||||
|
'extended' => 'boolean',
|
||||||
|
'hardcoded' => 'boolean',
|
||||||
|
'proper' => 'boolean',
|
||||||
|
'repack' => 'boolean',
|
||||||
|
'widescreen' => 'boolean',
|
||||||
|
'unrated' => 'boolean',
|
||||||
|
'3d' => 'boolean'
|
||||||
|
);
|
||||||
|
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
public function parse($name) {
|
||||||
|
$this->parts = array();
|
||||||
|
$this->torrent = array('name' => $name);
|
||||||
|
$this->excess_raw = $name;
|
||||||
|
$this->group_raw = '';
|
||||||
|
$this->start = 0;
|
||||||
|
$this->end = null;
|
||||||
|
$this->title_raw = null;
|
||||||
|
|
||||||
|
foreach($this->patterns as $patterns_single) {
|
||||||
|
foreach($patterns_single as $key => $pattern) {
|
||||||
|
if(!in_array($key, array('season', 'episode', 'website'))) {
|
||||||
|
$pattern = "\b{$pattern}\b";
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean_name = str_replace('_', ' ', $this->torrent['name']);
|
||||||
|
if(preg_match("/{$pattern}/i", $clean_name, $match) == 0) break;
|
||||||
|
|
||||||
|
$index = array();
|
||||||
|
if(is_array($match)) {
|
||||||
|
array_shift($match);
|
||||||
|
}
|
||||||
|
if(sizeof($match) == 0) break;
|
||||||
|
if(sizeof($match) > 1) {
|
||||||
|
$index['raw'] = 0;
|
||||||
|
$index['clean'] = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$index['raw'] = 0;
|
||||||
|
$index['clean'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($this->types[$key]) && $this->types[$key] == 'boolean') {
|
||||||
|
$clean = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$clean = $match[$index['clean']];
|
||||||
|
if(isset($this->types[$key]) && $this->types[$key] == 'integer') {
|
||||||
|
$clean = (int)$clean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($key == 'group') {
|
||||||
|
if((isset($this->patterns[5][1]) && preg_match_all("/{$this->patterns[5][1]}/i", $clean)) ||
|
||||||
|
(isset($this->patterns[4][1]) && preg_match_all("/{$this->patterns[4][1]}/", $clean))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(preg_match('/[^ ]+ [^ ]+ .+/', $clean)) {
|
||||||
|
$key = 'episodeName';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($key == 'episode') {
|
||||||
|
$sub_pattern = $this->escape_regex($match[$index['raw']]);
|
||||||
|
$this->torrent['map'] = preg_replace("/{$sub_pattern}/", '{episode}', $this->torrent['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->part($key, $match, $match[$index['raw']], $clean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = $this->torrent['name'];
|
||||||
|
if(!is_null($this->end)) {
|
||||||
|
$raw = explode('(', substr($raw, $this->start, $this->end - $this->start));
|
||||||
|
$raw = $raw[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean = preg_replace("/^ -/", '', $raw);
|
||||||
|
if(strpos($clean, ' ') === false && strpos($clean, '.') !== false) {
|
||||||
|
$clean = str_replace('.', ' ', $clean);
|
||||||
|
}
|
||||||
|
$clean = str_replace('_', ' ', $clean);
|
||||||
|
$clean = trim(preg_replace("/([\[\(_]|- )$/", '', $clean));
|
||||||
|
|
||||||
|
$this->part('title', array(), $raw, $clean);
|
||||||
|
|
||||||
|
$clean = preg_replace("/(^[-\. ()]+)|([-\. ]+$)/", '', $this->excess_raw);
|
||||||
|
$clean = preg_replace("/[\(\)\/]/", ' ', $clean);
|
||||||
|
$match = preg_split("/\.\.+| +/", $clean);
|
||||||
|
if(sizeof($match) > 0 && is_array($match[0])) {
|
||||||
|
$match = $match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean = $match;
|
||||||
|
$clean = array_filter($clean, function($var) {
|
||||||
|
return $var != '-' ? true : false;
|
||||||
|
});
|
||||||
|
$clean = array_filter($clean, function($var) {
|
||||||
|
return trim($var, '-');
|
||||||
|
});
|
||||||
|
$clean = array_values($clean);
|
||||||
|
|
||||||
|
if(sizeof($clean) > 0) {
|
||||||
|
$group_pattern = $clean[sizeof($clean) - 1] . $this->group_raw;
|
||||||
|
if(strpos($this->torrent['name'], $group_pattern) == strlen($this->torrent['name']) - strlen($group_pattern)) {
|
||||||
|
$this->late('group', array_pop($clean) . $this->group_raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($this->torrent['map']) && sizeof($clean) > 0) {
|
||||||
|
$episode_name_pattern = '{episode}' . preg_replace("/_+$/", '', $clean[0]);
|
||||||
|
|
||||||
|
if(strpos($this->torrent['map'], $episode_name_pattern) != -1) {
|
||||||
|
$this->late('episodeName', array_shift($clean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sizeof($clean) != 0) {
|
||||||
|
if(sizeof($clean) == 1) {
|
||||||
|
$clean = $clean[0];
|
||||||
|
}
|
||||||
|
$this->part('excess', array(), $this->excess_raw, $clean);
|
||||||
|
}
|
||||||
|
return $this->parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function escape_regex($subject) {
|
||||||
|
return preg_replace("/[\-\[\]{}()*+?.,\\\^$|#\s]/", "\\\\$&", $subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function part($name, $match, $raw, $clean) {
|
||||||
|
# The main core instructuions
|
||||||
|
$this->parts[$name] = $clean;
|
||||||
|
|
||||||
|
if(sizeof($match) > 0) {
|
||||||
|
# The instructions for extracting title
|
||||||
|
$index = strpos($this->torrent['name'], $match[0]);
|
||||||
|
if($index == 0) {
|
||||||
|
$this->start = strlen($match[0]);
|
||||||
|
}
|
||||||
|
elseif(is_null($this->end) || $index < $this->end) {
|
||||||
|
$this->end = $index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($name != 'excess') {
|
||||||
|
if($name == 'group') {
|
||||||
|
$this->group_raw = $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_null($raw)) {
|
||||||
|
$this->excess_raw = str_replace($raw, '', $this->excess_raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function late($name, $clean) {
|
||||||
|
if($name == 'group') {
|
||||||
|
$this->part($name, array(), null, $clean);
|
||||||
|
}
|
||||||
|
elseif($name == 'episodeName') {
|
||||||
|
$clean = preg_replace("/[\._]/", ' ', $clean);
|
||||||
|
$clean = preg_replace("/_+$/", '', $clean);
|
||||||
|
$this->part($name, array(), null, trim($clean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
|||||||
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
use App\Torrentio\Action\Handler\GetTvShowOptionsHandler;
|
||||||
use App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use App\User\Framework\Repository\UserRepository;
|
use App\User\Framework\Repository\UserRepository;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|||||||
@@ -3,92 +3,80 @@
|
|||||||
namespace App\Download;
|
namespace App\Download;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
|
||||||
use App\Torrentio\Result\TorrentioResult;
|
use App\Torrentio\Result\TorrentioResult;
|
||||||
use App\User\Dto\UserPreferences;
|
use App\User\Dto\UserPreferences;
|
||||||
|
|
||||||
class DownloadOptionEvaluator
|
class DownloadOptionEvaluator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Monitor $monitor
|
|
||||||
* @param TorrentioResult[] $results
|
* @param TorrentioResult[] $results
|
||||||
|
* @param UserPreferences $filter
|
||||||
* @return TorrentioResult|null
|
* @return TorrentioResult|null
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function evaluateOptions(array $results, UserPreferences $userPreferences): ?TorrentioResult
|
public function evaluateOptions(array $results, UserPreferences $filter): ?TorrentioResult
|
||||||
{
|
{
|
||||||
$sizeLow = 000;
|
$matches = Map::from($results)->filter(function ($result) use ($filter) {
|
||||||
$sizeHigh = 4096;
|
if (false === $this->validateFilterItems($result, $filter)) {
|
||||||
|
return false;
|
||||||
$bestMatches = [];
|
|
||||||
$matches = [];
|
|
||||||
|
|
||||||
foreach ($results as $result) {
|
|
||||||
if (!in_array($userPreferences->language, $result->languages)) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result->resolution === $userPreferences->resolution
|
if (false === $this->validateSize($result, $filter)) {
|
||||||
&& $result->codec === $userPreferences->codec
|
return false;
|
||||||
) {
|
|
||||||
$bestMatches[] = $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userPreferences->resolution === '2160p'
|
return true;
|
||||||
&& $userPreferences->codec === $result->codec
|
});
|
||||||
&& $result->resolution === '1080p'
|
|
||||||
) {
|
|
||||||
$matches[] = $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($userPreferences->codec === 'h264'
|
if ($matches->count() > 0) {
|
||||||
&& $userPreferences->resolution === $result->resolution
|
return Map::from($matches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
||||||
&& $result->codec === 'h265'
|
|
||||||
) {
|
|
||||||
$matches[] = $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($userPreferences->codec === null )
|
|
||||||
&& ($userPreferences->resolution === null )) {
|
|
||||||
$matches[] = $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sizeMatches = [];
|
|
||||||
|
|
||||||
foreach ($bestMatches as $result) {
|
|
||||||
if (str_contains($result->size, 'GB')) {
|
|
||||||
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
|
||||||
} else {
|
|
||||||
$size = (int) trim(str_replace('MB', '', $result->size));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($size > $sizeLow && $size < $sizeHigh) {
|
|
||||||
$sizeMatches[] = $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($sizeMatches)) {
|
|
||||||
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($matches as $result) {
|
|
||||||
$size = 0;
|
|
||||||
if (str_contains($result->size, 'GB')) {
|
|
||||||
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
|
||||||
} else {
|
|
||||||
$size = (int) trim(str_replace('MB', '', $result->size));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($size > $sizeLow && $size < $sizeHigh) {
|
|
||||||
$sizeMatches[] = $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($sizeMatches)) {
|
|
||||||
return Map::from($sizeMatches)->usort(fn($a, $b) => $a->seeders <=> $b->seeders)->last();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function validateFilterItems(TorrentioResult $result, UserPreferences $filter): bool
|
||||||
|
{
|
||||||
|
if (array_intersect($filter->language, $result->languages) === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$valid = true;
|
||||||
|
|
||||||
|
if (null !== $filter->resolution && !in_array($result->resolution, $filter->resolution)) {
|
||||||
|
$valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $filter->codec && in_array($result->codec, $filter->codec)) {
|
||||||
|
$valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $filter->quality && in_array($result->quality, $filter->quality)) {
|
||||||
|
$valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $filter->provider && in_array($result->provider, $filter->provider)) {
|
||||||
|
$valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateSize(TorrentioResult $result, UserPreferences $filter): bool
|
||||||
|
{
|
||||||
|
$sizeLow = 000;
|
||||||
|
$sizeHigh = 4096;
|
||||||
|
|
||||||
|
if (str_contains($result->size, 'GB')) {
|
||||||
|
$size = (int) trim(str_replace('GB', '', $result->size)) * 1024;
|
||||||
|
} else {
|
||||||
|
$size = (int) trim(str_replace('MB', '', $result->size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size > $sizeLow && $size < $sizeHigh) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use App\Download\Framework\Repository\DownloadRepository;
|
|||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
use Gedmo\Timestampable\Traits\TimestampableEntity;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||||
use Symfony\UX\Turbo\Attribute\Broadcast;
|
use Symfony\UX\Turbo\Attribute\Broadcast;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use App\Download\Framework\Entity\Download;
|
|||||||
use App\User\Framework\Entity\User;
|
use App\User\Framework\Entity\User;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use App\Base\Service\MediaFiles;
|
|||||||
use App\Library\Action\Command\LibrarySearchCommand;
|
use App\Library\Action\Command\LibrarySearchCommand;
|
||||||
use App\Library\Action\Result\LibrarySearchResult;
|
use App\Library\Action\Result\LibrarySearchResult;
|
||||||
use App\Library\Dto\MediaFileDto;
|
use App\Library\Dto\MediaFileDto;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use App\Monitor\Framework\Repository\MonitorRepository;
|
|||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Tmdb\Tmdb;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
use OneToMany\RichBundle\Contract\HandlerInterface;
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
use OneToMany\RichBundle\Contract\ResultInterface;
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Search\Action\Handler;
|
namespace App\Search\Action\Handler;
|
||||||
|
|
||||||
|
use App\Base\Util\ImdbMatcher;
|
||||||
|
use App\Search\Action\Result\RedirectToMediaResult;
|
||||||
use App\Search\Action\Result\SearchResult;
|
use App\Search\Action\Result\SearchResult;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use OneToMany\RichBundle\Contract\CommandInterface;
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
@@ -17,6 +19,13 @@ class SearchHandler implements HandlerInterface
|
|||||||
|
|
||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
|
if (ImdbMatcher::isMatch($command->term)) {
|
||||||
|
$result = $this->tmdb->findByImdbId($command->term);
|
||||||
|
return new RedirectToMediaResult(
|
||||||
|
imdbId: $result->imdbId,
|
||||||
|
mediaType: $result->mediaType,
|
||||||
|
);
|
||||||
|
}
|
||||||
return new SearchResult(
|
return new SearchResult(
|
||||||
term: $command->term,
|
term: $command->term,
|
||||||
results: $this->tmdb->search($command->term)
|
results: $this->tmdb->search($command->term)
|
||||||
|
|||||||
13
src/Search/Action/Result/RedirectToMediaResult.php
Normal file
13
src/Search/Action/Result/RedirectToMediaResult.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Search\Action\Result;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface;
|
||||||
|
|
||||||
|
class RedirectToMediaResult implements ResultInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $imdbId,
|
||||||
|
public string $mediaType,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Search\Action\Handler\GetMediaInfoHandler;
|
|||||||
use App\Search\Action\Handler\SearchHandler;
|
use App\Search\Action\Handler\SearchHandler;
|
||||||
use App\Search\Action\Input\GetMediaInfoInput;
|
use App\Search\Action\Input\GetMediaInfoInput;
|
||||||
use App\Search\Action\Input\SearchInput;
|
use App\Search\Action\Input\SearchInput;
|
||||||
|
use App\Search\Action\Result\RedirectToMediaResult;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
||||||
@@ -28,6 +29,13 @@ final class WebController extends AbstractController
|
|||||||
): Response {
|
): Response {
|
||||||
$results = $this->searchHandler->handle($searchInput->toCommand());
|
$results = $this->searchHandler->handle($searchInput->toCommand());
|
||||||
|
|
||||||
|
if ($results instanceof RedirectToMediaResult) {
|
||||||
|
return $this->redirectToRoute('app_search_result', [
|
||||||
|
'mediaType' => $results->mediaType,
|
||||||
|
'imdbId' => $results->imdbId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('search/results.html.twig', [
|
return $this->render('search/results.html.twig', [
|
||||||
'results' => $results,
|
'results' => $results,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Tmdb\Framework\Controller;
|
namespace App\Tmdb\Framework\Controller;
|
||||||
|
|
||||||
|
use App\Base\Util\ImdbMatcher;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use App\Tmdb\TmdbResult;
|
use App\Tmdb\TmdbResult;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -17,17 +18,28 @@ class ApiController extends AbstractController
|
|||||||
$results = [];
|
$results = [];
|
||||||
|
|
||||||
$term = $request->query->get('query') ?? null;
|
$term = $request->query->get('query') ?? null;
|
||||||
|
$term = trim($term);
|
||||||
|
|
||||||
if (null !== $term) {
|
if (null !== $term) {
|
||||||
$tmdbResults = $tmdb->search($term);
|
if (ImdbMatcher::isMatch($term)) {
|
||||||
|
$tmdbResult = $tmdb->findByImdbId($term);
|
||||||
foreach ($tmdbResults as $tmdbResult) {
|
$results = [
|
||||||
/** @var TmdbResult $tmdbResult */
|
[
|
||||||
$results[] = [
|
'data' => $tmdbResult,
|
||||||
'data' => $tmdbResult,
|
'text' => $tmdbResult->title,
|
||||||
'text' => $tmdbResult->title,
|
'value' => "$tmdbResult->mediaType|$tmdbResult->imdbId",
|
||||||
'value' => "$tmdbResult->mediaType|$tmdbResult->imdbId",
|
]
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
$tmdbResults = $tmdb->search($term);
|
||||||
|
foreach ($tmdbResults as $tmdbResult) {
|
||||||
|
/** @var TmdbResult $tmdbResult */
|
||||||
|
$results[] = [
|
||||||
|
'data' => $tmdbResult,
|
||||||
|
'text' => $tmdbResult->title,
|
||||||
|
'value' => "$tmdbResult->mediaType|$tmdbResult->imdbId",
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,28 @@ class Tmdb
|
|||||||
throw new \Exception("No results found for $id");
|
throw new \Exception("No results found for $id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findByImdbId(string $imdbId)
|
||||||
|
{
|
||||||
|
$finder = new Find($this->client);
|
||||||
|
$result = $finder->findBy($imdbId, ['external_source' => 'imdb_id']);
|
||||||
|
|
||||||
|
if (count($result['movie_results']) > 0) {
|
||||||
|
$result = $result['movie_results'][0];
|
||||||
|
$mediaType = MediaType::Movie->value;
|
||||||
|
} elseif (count($result['tv_results']) > 0) {
|
||||||
|
$result = $result['tv_results'][0];
|
||||||
|
$mediaType = MediaType::TvShow->value;
|
||||||
|
} elseif (count($result['tv_episode_results']) > 0) {
|
||||||
|
$result = $result['tv_episode_results'][0];
|
||||||
|
$mediaType = MediaType::TvShow->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['media_type'] = $mediaType;
|
||||||
|
$result = $this->mediaDetails($imdbId, $result['media_type']);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
public function movieDetails(string $id)
|
public function movieDetails(string $id)
|
||||||
{
|
{
|
||||||
$client = new MovieRepository($this->client);
|
$client = new MovieRepository($this->client);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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,6 +9,7 @@ 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,7 +3,7 @@
|
|||||||
namespace App\Torrentio\Result;
|
namespace App\Torrentio\Result;
|
||||||
|
|
||||||
use App\User\Database\CountryLanguages;
|
use App\User\Database\CountryLanguages;
|
||||||
use Nihilarr\PTN;
|
use App\Base\Util\PTN;
|
||||||
|
|
||||||
class ResultFactory
|
class ResultFactory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,16 +9,21 @@ use App\User\Database\ResolutionList;
|
|||||||
use App\User\Dto\PreferenceOptions;
|
use App\User\Dto\PreferenceOptions;
|
||||||
use App\User\Dto\PreferenceOptionsFactory;
|
use App\User\Dto\PreferenceOptionsFactory;
|
||||||
use App\User\Dto\UserPreferencesFactory;
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
|
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\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||||
|
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
|
||||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||||
|
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||||
|
|
||||||
#[AsLiveComponent]
|
#[AsLiveComponent]
|
||||||
final class Filter extends AbstractController
|
final class Filter extends AbstractController
|
||||||
{
|
{
|
||||||
use DefaultActionTrait;
|
use DefaultActionTrait;
|
||||||
|
use ComponentWithFormTrait;
|
||||||
|
|
||||||
public array $preferences = [];
|
public array $preferences = [];
|
||||||
|
|
||||||
@@ -43,4 +48,9 @@ final class Filter extends AbstractController
|
|||||||
{
|
{
|
||||||
return CodecList::asSelectOptions();
|
return CodecList::asSelectOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function instantiateForm(): FormInterface
|
||||||
|
{
|
||||||
|
return $this->createForm(UserMediaPreferencesForm::class, UserPreferencesFactory::createFromUser($this->getUser()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class UtilExtension
|
|||||||
#[AsTwigFunction('episode_anchor')]
|
#[AsTwigFunction('episode_anchor')]
|
||||||
public function episodeAnchor($season, $episode): ?string
|
public function episodeAnchor($season, $episode): ?string
|
||||||
{
|
{
|
||||||
return "episode_" . $season . "_" . $episode;
|
return "episode_" . (int) $season . "_" . (int) $episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[AsTwigFunction('extract_from_episode_id')]
|
#[AsTwigFunction('extract_from_episode_id')]
|
||||||
@@ -80,7 +80,7 @@ class UtilExtension
|
|||||||
|
|
||||||
// Capture season
|
// Capture season
|
||||||
$seasonMatch = [];
|
$seasonMatch = [];
|
||||||
preg_match('/[sS]\d\d/', $episodeId, $seasonMatch);
|
preg_match('/[sS]\d\d(\d)?(\d)?/', $episodeId, $seasonMatch);
|
||||||
if (empty($seasonMatch)) {
|
if (empty($seasonMatch)) {
|
||||||
$season = "";
|
$season = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -89,7 +89,7 @@ class UtilExtension
|
|||||||
|
|
||||||
// Capture episode
|
// Capture episode
|
||||||
$episodeMatch = [];
|
$episodeMatch = [];
|
||||||
preg_match('/[eE]\d\d/', $episodeId, $episodeMatch);
|
preg_match('/[eE]\d\d(\d)?(\d)?/', $episodeId, $episodeMatch);
|
||||||
if (empty($episodeMatch)) {
|
if (empty($episodeMatch)) {
|
||||||
$episode = "";
|
$episode = "";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ class SaveUserMediaPreferencesCommand implements CommandInterface
|
|||||||
public static function fromUserMediaPreferencesForm(FormInterface $form): self
|
public static function fromUserMediaPreferencesForm(FormInterface $form): self
|
||||||
{
|
{
|
||||||
return new static(
|
return new static(
|
||||||
resolution: $form->get('resolution')->getData(),
|
resolution: \implode(',', $form->get('resolution')->getData()),
|
||||||
codec: $form->get('codec')->getData(),
|
codec: \implode(',', $form->get('codec')->getData()),
|
||||||
quality: $form->get('quality')->getData(),
|
quality: \implode(',', $form->get('quality')->getData()),
|
||||||
language: $form->get('language')->getData(),
|
language: \implode(',', $form->get('language')->getData()),
|
||||||
provider: $form->get('provider')->getData(),
|
provider: \implode(',', $form->get('provider')->getData()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,10 +16,9 @@ class CodecList
|
|||||||
|
|
||||||
public static function asSelectOptions(): array
|
public static function asSelectOptions(): array
|
||||||
{
|
{
|
||||||
$result = [];
|
return [
|
||||||
foreach (static::$codecs as $codec) {
|
'h264' => 'h264',
|
||||||
$result[$codec] = $codec;
|
'h265/HEVC' => 'h265',
|
||||||
}
|
];
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ class UserPreferences
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly ?string $resolution,
|
public readonly ?array $resolution,
|
||||||
public readonly ?string $codec,
|
public readonly ?array $codec,
|
||||||
public readonly ?string $language,
|
public readonly ?array $language,
|
||||||
public readonly ?string $provider,
|
public readonly ?array $provider,
|
||||||
public readonly ?string $quality,
|
public readonly ?array $quality,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class UserPreferencesFactory
|
|||||||
if ($value === "") {
|
if ($value === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
$value = explode(',', $value);
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Framework\Controller\Api;
|
||||||
|
|
||||||
|
use App\User\Dto\UserPreferences;
|
||||||
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/user/filters')]
|
||||||
|
class UserFilterApiController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('', 'api.user.filters', methods: ['GET'])]
|
||||||
|
public function getFilters(): Response
|
||||||
|
{
|
||||||
|
return $this->json([
|
||||||
|
UserPreferencesFactory::createFromUser($this->getUser())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,8 +52,7 @@ class PreferencesController extends AbstractController
|
|||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$form = $this->createForm(UserMediaPreferencesForm::class);
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,30 @@ 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\Database\ResolutionList;
|
||||||
|
use App\User\Dto\UserPreferences;
|
||||||
|
use App\User\Dto\UserPreferencesFactory;
|
||||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Event\PreSetDataEvent;
|
||||||
|
use Symfony\Component\Form\Event\PreSubmitEvent;
|
||||||
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;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Routing\Generator\UrlGenerator;
|
use Symfony\Component\Routing\Generator\UrlGenerator;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
class UserMediaPreferencesForm extends AbstractType
|
class UserMediaPreferencesForm extends AbstractType
|
||||||
{
|
{
|
||||||
|
private UserPreferences $userPreferences;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UrlGeneratorInterface $urlGenerator,
|
private readonly UrlGeneratorInterface $urlGenerator,
|
||||||
) {}
|
private readonly Security $security,
|
||||||
|
) {
|
||||||
|
$this->userPreferences = UserPreferencesFactory::createFromUser($security->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
@@ -34,10 +45,20 @@ class UserMediaPreferencesForm extends AbstractType
|
|||||||
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
|
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
|
||||||
{
|
{
|
||||||
$question = [
|
$question = [
|
||||||
'attr' => ['class' => 'w-64 text-input mb-4'],
|
'attr' => [
|
||||||
'label_attr' => ['class' => 'w-64 text-white block font-semibold mb-2'],
|
'class' => 'min-w-24 text-input mb-4',
|
||||||
|
'data-result-filter-target' => $fieldName,
|
||||||
|
'data-controller' => 'symfony--ux-autocomplete--autocomplete',
|
||||||
|
'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value' => '{"highlight":false}',
|
||||||
|
'data-preferred' => \json_encode($this->userPreferences->$fieldName),
|
||||||
|
],
|
||||||
|
'row_attr' => [
|
||||||
|
'class' => 'filter-label text-white'
|
||||||
|
],
|
||||||
|
'label_attr' => ['class' => 'block font-semibold mb-2'],
|
||||||
'choices' => $this->addDefaultChoice($choices),
|
'choices' => $this->addDefaultChoice($choices),
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'multiple' => true,
|
||||||
];
|
];
|
||||||
$builder->add($fieldName, ChoiceType::class, $question);
|
$builder->add($fieldName, ChoiceType::class, $question);
|
||||||
}
|
}
|
||||||
@@ -46,11 +67,14 @@ class UserMediaPreferencesForm extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'),
|
'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'),
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'filter-items w-full p-4 bg-black/20 border-2 border-orange-500 text-md dark:text-gray-50 rounded-lg',
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addDefaultChoice(array $choices): iterable
|
private function addDefaultChoice(array $choices): iterable
|
||||||
{
|
{
|
||||||
return ['n/a' => ''] + $choices;
|
return ['n/a' => 'n/a'] + $choices;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ module.exports = {
|
|||||||
"bg-orange-400",
|
"bg-orange-400",
|
||||||
"bg-blue-600",
|
"bg-blue-600",
|
||||||
"bg-rose-600",
|
"bg-rose-600",
|
||||||
|
"bg-black/20",
|
||||||
"alert-success",
|
"alert-success",
|
||||||
"alert-warning",
|
"alert-warning",
|
||||||
|
"font-bold",
|
||||||
"min-w-64",
|
"min-w-64",
|
||||||
"rotate-180",
|
"rotate-180",
|
||||||
"-rotate-180",
|
"-rotate-180",
|
||||||
|
|||||||
@@ -14,14 +14,11 @@
|
|||||||
{% 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="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:{{ 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 }}">
|
||||||
|
|||||||
@@ -29,12 +29,13 @@
|
|||||||
<td class="whitespace-nowrap gap-2 text-sm align-middle text-gray-800 dark:text-gray-50">
|
<td class="whitespace-nowrap gap-2 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 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-16 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>
|
||||||
|
<div class="number text-black font-bold text-center z-40"
|
||||||
|
>{{ download.progress }}%</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-black font-bold text-center z-40 ml-[-42px]">{{ download.progress }}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -4,110 +4,69 @@
|
|||||||
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 movie-results:optionsLoaded@window->result-filter#loadOptions tv-results:optionsLoaded@window->result-filter#loadOptions action-button:downloadSeason@window->result-filter#downloadSeason"
|
data-action="change->result-filter#filter 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">
|
|
||||||
<label for="resolution">
|
{% set preferences_form = form %}
|
||||||
Resolution
|
{{ form_start(preferences_form) }}
|
||||||
<select id="resolution"
|
<h3 class="font-bold text-lg mb-2 md:mb-4">Apply a filter to your results</h3>
|
||||||
data-result-filter-target="resolution"
|
<div class="flex flex-col md:flex-row gap-2 justify-between">
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
{{ form_row(preferences_form.resolution) }}
|
||||||
value="{{ app.user.userPreferenceValues["resolution"] }}"
|
{{ form_row(preferences_form.codec) }}
|
||||||
>
|
{{ form_row(preferences_form.language) }}
|
||||||
<option value="">n/a</option>
|
{{ form_row(preferences_form.provider) }}
|
||||||
{% for name, value in this.resolutionOptions %}
|
{{ form_row(preferences_form.quality) }}
|
||||||
<option value="{{ value }}"
|
|
||||||
{{ value == this.userPreferences['resolution'] ? 'selected' }}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
>{{ name }}</option>
|
<div class="flex flex-col gap-1 md:gap-3">
|
||||||
{% endfor %}
|
<label for="season">
|
||||||
</select>
|
Season
|
||||||
</label>
|
</label>
|
||||||
<label for="codec">
|
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 mb-4 py-1 md:py-2 text-center bg-orange-500 rounded-ms text-black"
|
||||||
Codec
|
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
||||||
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
|
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
||||||
<option value="">n/a</option>
|
>
|
||||||
{% for name, value in this.codecOptions %}
|
{% for season in range(1, results.media.episodes|length) %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ season }}"
|
||||||
{{ value == this.userPreferences['codec'] ? 'selected' }}
|
{% if results.season == season %}
|
||||||
>{{ name }}</option>
|
selected="selected"
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</select>
|
>{{ season }}</option>
|
||||||
</label>
|
{% endfor %}
|
||||||
<label for="language">
|
</select>
|
||||||
Language
|
</div>
|
||||||
<select id="language"
|
{% endif %}
|
||||||
data-result-filter-target="language"
|
</div>
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
{{ form_end(preferences_form) }}
|
||||||
{% if this.userPreferences['language'] != null %}
|
|
||||||
data-preferred="{{ this.userPreferences['language'] }}"
|
<div class="flex flex-col md:flex-row justify-between">
|
||||||
{% endif %}
|
<span
|
||||||
>
|
{{ stimulus_target('result-filter', 'loadingIcon') }}
|
||||||
</select>
|
{{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
|
||||||
</label>
|
|
||||||
<label for="provider">
|
|
||||||
Provider
|
|
||||||
<select id="provider"
|
|
||||||
data-result-filter-target="provider"
|
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
|
||||||
{% if this.userPreferences['provider'] != null %}
|
|
||||||
data-preferred="{{ this.userPreferences['provider'] }}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label for="quality">
|
|
||||||
Quality
|
|
||||||
<select id="quality"
|
|
||||||
data-result-filter-target="quality"
|
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
|
||||||
{% if this.userPreferences['quality'] != null %}
|
|
||||||
data-preferred="{{ this.userPreferences['quality'] }}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
|
||||||
<label for="season">
|
|
||||||
Season
|
|
||||||
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
|
||||||
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
|
||||||
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
|
||||||
>
|
|
||||||
{% for season in range(1, results.media.episodes|length) %}
|
|
||||||
<option value="{{ season }}"
|
|
||||||
{% if results.season == season %}
|
|
||||||
selected="selected"
|
|
||||||
{% endif %}
|
|
||||||
>{{ season }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{% endif %}
|
|
||||||
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
|
|
||||||
class="loading-icon">
|
class="loading-icon">
|
||||||
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
|
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
|
<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>
|
||||||
|
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
|
||||||
|
the appropriate file(s).
|
||||||
|
<br /><br />
|
||||||
|
Do you wish to download <strong>season <span id="downloadSeasonModal">{{ results.season }}</span></strong> of "<strong>{{ results.media.title }}</strong>"?
|
||||||
|
</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"
|
||||||
|
{{ stimulus_target('result_filter', 'downloadSelected') }}
|
||||||
|
{{ stimulus_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
||||||
|
>Download Selected</button>
|
||||||
|
|
||||||
|
<input type="checkbox" name="selectAll" id="selectAll"
|
||||||
|
{{ stimulus_target('result_filter', 'selectAll') }}
|
||||||
|
{{ stimulus_action('result_filter', 'selectAllEpisodes', 'change') }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
|
||||||
<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 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
|
|
||||||
<a href="{{ path('app_user_preferences') }}" class="text-underline">preferences</a> to choose
|
|
||||||
the appropriate file(s).
|
|
||||||
<br /><br />
|
|
||||||
Do you wish to download <strong>season <span id="downloadSeasonModal">{{ results.season }}</span></strong> of "<strong>{{ results.media.title }}</strong>"?
|
|
||||||
</twig:Modal>
|
|
||||||
|
|
||||||
<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_action('result_filter', 'downloadSelectedEpisodes', 'click') }}
|
|
||||||
>Download Selected</button>
|
|
||||||
|
|
||||||
<input type="checkbox" name="selectAll" id="selectAll"
|
|
||||||
{{ stimulus_target('result_filter', 'selectAll') }}
|
|
||||||
{{ stimulus_action('result_filter', 'selectAllEpisodes', 'change') }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
>
|
>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
|
id="search-button"
|
||||||
class="absolute top-1 right-1 flex items-center rounded
|
class="absolute top-1 right-1 flex items-center rounded
|
||||||
bg-green-600 py-1 px-2.5 border border-transparent text-center
|
bg-green-600 py-1 px-2.5 border border-transparent text-center
|
||||||
text-sm text-white transition-all
|
text-sm text-white transition-all
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
|
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
{{ title }} - {{ year }}
|
{{ title }} ({{ year }})
|
||||||
</h3>
|
</h3>
|
||||||
<p class="hidden md:block md:text-gray-50">
|
<p class="hidden md:block md:text-gray-50">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
|||||||
8
templates/components/SubmitButton.html.twig
Normal file
8
templates/components/SubmitButton.html.twig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<button class="submit-button flex flex-row gap-2 items-center">
|
||||||
|
{% if show_icon|default %}
|
||||||
|
<twig:ux:icon name="zondicons:checkmark" width=".8rem" class="text-green-500" />
|
||||||
|
{% endif %}
|
||||||
|
{% if false == icon_only|default(false) %}
|
||||||
|
{{ text|default('submit') }}
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
>
|
>
|
||||||
<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.getEpisodes().items %}
|
||||||
<div id="{{ episode_anchor(episode['season_number'], episode['episode_number']) }}" class="results"
|
<episode-container id="{{ episode_anchor(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', {
|
||||||
@@ -15,77 +16,73 @@
|
|||||||
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/60 bg-clip-padding backdrop-filter backdrop-blur-md 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="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"
|
<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'] }}.">
|
||||||
{{ stimulus_action('tv-results', 'toggleList', 'click') }}
|
<span class="results-count-number" {{ stimulus_target('tv-results', 'count') }}>-</span> results
|
||||||
>
|
</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="Air date {{ episode['name'] }}">
|
<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') }}.'>
|
||||||
{{ 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="flex flex-col gap-4 justify-between">
|
<div class="results-container inline-block overflow-hidden rounded-lg hidden">
|
||||||
<div class="flex flex-col items-center">
|
<twig:Turbo:Frame id="results_{{ episode_id(episode['season_number'], episode['episode_number']) }}" src="{{ path('app_torrentio_tvshows', {
|
||||||
<input type="checkbox"
|
tmdbId: this.tmdbId,
|
||||||
{{ stimulus_target('tv-results', 'episodeSelector') }}
|
imdbId: this.imdbId,
|
||||||
/>
|
season: episode['season_number'],
|
||||||
</div>
|
episode: episode['episode_number'],
|
||||||
<button class="flex flex-col items-end transition-transform duration-300 ease-in-out rotate-90"
|
target: 'results_' ~ episode_id(episode['season_number'], episode['episode_number']),
|
||||||
{{ stimulus_target('tv-results', 'toggleButton') }}
|
block: 'tvshow_results'
|
||||||
{{ 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>
|
||||||
<div class="inline-block overflow-hidden rounded-lg">
|
</episode-container>
|
||||||
<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 %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="mb-4 flex flex-row gap-2 justify-between">
|
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||||
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
{{ results.media.title }} - {{ results.media.year }}
|
{{ results.media.title }} ({{ results.media.year }})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
@@ -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="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-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 id="movie_results_count">-</span> results
|
<span class="results-count-number" 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 %}
|
||||||
<div class="results"
|
<movie-container 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,7 +91,7 @@
|
|||||||
target: 'movie_results_frame',
|
target: 'movie_results_frame',
|
||||||
block: 'movie_results'
|
block: 'movie_results'
|
||||||
}) }}" />
|
}) }}" />
|
||||||
</div>
|
</movie-container>
|
||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
results="results"
|
results="results"
|
||||||
|
|||||||
@@ -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 {{ results.media.mediaType == "tvshows" ? "hidden" : "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 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,7 +41,29 @@
|
|||||||
</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 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 %}>
|
<tr is="dl-tr"
|
||||||
|
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>
|
||||||
@@ -64,17 +86,7 @@
|
|||||||
{{ 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">
|
||||||
|
|||||||
@@ -6,16 +6,24 @@
|
|||||||
<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-4">Define a filter to be pre-applied to your download options.</p>
|
||||||
{{ form_start(preferences_form) }}
|
<div id="filter">
|
||||||
{{ form_row(preferences_form.language) }}
|
{{ form_start(preferences_form) }}
|
||||||
{{ form_row(preferences_form.quality) }}
|
<div class="flex flex-col md:flex-row gap-2">
|
||||||
{{ form_row(preferences_form.provider) }}
|
{{ form_row(preferences_form.resolution) }}
|
||||||
{{ form_row(preferences_form.resolution) }}
|
{{ form_row(preferences_form.codec) }}
|
||||||
{{ form_row(preferences_form.codec) }}
|
{{ form_row(preferences_form.language) }}
|
||||||
<button class="submit-button">Save</button>
|
{{ form_row(preferences_form.provider) }}
|
||||||
{{ form_end(preferences_form) }}
|
{{ form_row(preferences_form.quality) }}
|
||||||
|
<div class="self-end mb-4">
|
||||||
|
<twig:SubmitButton show_icon text="Save"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form_end(preferences_form) }}
|
||||||
|
</div>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||||
<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-4">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') }}">
|
||||||
|
|||||||
Reference in New Issue
Block a user