Compare commits
5 Commits
dev-web-co
...
0e13b74b3b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e13b74b3b | |||
| f9ec089f8b | |||
| 87e72ec55e | |||
| 23a88ec6bb | |||
| d33a961f2d |
@@ -1,6 +1,15 @@
|
|||||||
export default class DownloadOptionTr extends HTMLTableRowElement {
|
export default class DownloadOptionTr extends HTMLTableRowElement {
|
||||||
H264_CODECS = ['h264', 'h.264', 'x264']
|
H264_CODECS = {
|
||||||
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
|
'h264': 'h264',
|
||||||
|
'h.264': 'h264',
|
||||||
|
'x264': 'h264',
|
||||||
|
}
|
||||||
|
H265_CODECS = {
|
||||||
|
'h265': 'h265',
|
||||||
|
'h.265': 'h265',
|
||||||
|
'x265': 'h265',
|
||||||
|
'hevc': 'h265',
|
||||||
|
}
|
||||||
|
|
||||||
#downloadBtnEl;
|
#downloadBtnEl;
|
||||||
#selectEpisodeInputEl;
|
#selectEpisodeInputEl;
|
||||||
@@ -53,13 +62,6 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
|
|||||||
|
|
||||||
filter({ detail: { activeFilter } }) {
|
filter({ detail: { activeFilter } }) {
|
||||||
const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`)
|
const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`)
|
||||||
const props = {
|
|
||||||
"resolution": this.resolution.trim(),
|
|
||||||
"codec": this.codec.trim(),
|
|
||||||
"provider": this.provider.trim(),
|
|
||||||
"languages": this.languages,
|
|
||||||
"quality": this.quality,
|
|
||||||
}
|
|
||||||
|
|
||||||
let include = true;
|
let include = true;
|
||||||
this.classList.add('r-tablerow');
|
this.classList.add('r-tablerow');
|
||||||
@@ -69,25 +71,24 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
|
|||||||
|
|
||||||
this.querySelector('input[type="checkbox"]').checked = false;
|
this.querySelector('input[type="checkbox"]').checked = false;
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(activeFilter)) {
|
if (!this.#validateResolutions(activeFilter.resolution)) {
|
||||||
if (value === "" || key === "season") {
|
include = false;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
if (key === "codec" && value === "h264") {
|
if (!this.#validateCodecs(activeFilter.codec)) {
|
||||||
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
|
include = false;
|
||||||
include = false;
|
}
|
||||||
}
|
|
||||||
} else if (key === "codec" && value === "h265") {
|
if (!this.#validateLanguages(activeFilter.language)) {
|
||||||
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
|
include = false;
|
||||||
include = false;
|
}
|
||||||
}
|
|
||||||
} else if (key === "language") {
|
if (!this.#validateQualities(activeFilter.quality)) {
|
||||||
if (!props["languages"].includes(value)) {
|
include = false;
|
||||||
include = false;
|
}
|
||||||
}
|
|
||||||
} else if (props[key] !== value) {
|
if (!this.#validateProviders(activeFilter.provider)) {
|
||||||
include = false;
|
include = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === include) {
|
if (false === include) {
|
||||||
@@ -121,4 +122,66 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
|
|||||||
console.log(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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default class extends Controller {
|
|||||||
"quality": "",
|
"quality": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultOptions = '<option value="">n/a</option><option value="-">-</option>';
|
||||||
|
|
||||||
static outlets = ['tv-episode-list']
|
static outlets = ['tv-episode-list']
|
||||||
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
|
||||||
static values = {
|
static values = {
|
||||||
@@ -60,92 +62,49 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addLanguages(option) {
|
addLanguages(option) {
|
||||||
const languages = Object.assign([], option.languages);
|
option.languages.forEach((language) => {
|
||||||
languages.forEach((language) => {
|
|
||||||
if (!this.languages.includes(language)) {
|
if (!this.languages.includes(language)) {
|
||||||
this.languages.push(language);
|
this.languages.push(language);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const preferred = JSON.parse(this.languageTarget.dataset.preferred);
|
||||||
const preferred = this.languageTarget.dataset.preferred;
|
this.languageTarget.innerHTML = this.#serializeSelectOptions(this.languages);
|
||||||
if (preferred) {
|
this.languageTarget.tomselect.items = 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) {
|
addProviders(option) {
|
||||||
if (!this.providers.includes(option.provider)) {
|
if (!this.providers.includes(option.provider)) {
|
||||||
this.providers.push(option.provider);
|
this.providers.push(option.provider);
|
||||||
}
|
}
|
||||||
|
const preferred = JSON.parse(this.providerTarget.dataset.preferred);
|
||||||
const preferred = this.providerTarget.dataset.preferred;
|
this.providerTarget.innerHTML = this.#serializeSelectOptions(this.providers);
|
||||||
if (preferred) {
|
this.providerTarget.tomselect.items = 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) {
|
addQualities(option) {
|
||||||
if (!this.qualities.includes(option.quality)) {
|
if (option.quality.toLowerCase() in this.reverseMappedQualitiesValue) {
|
||||||
if (option.quality.toLowerCase() in this.reverseMappedQualitiesValue) {
|
let quality = this.reverseMappedQualitiesValue[option.quality.toLowerCase()];
|
||||||
this.qualities.push(option.quality);
|
// converts api returned quality with a value the system recognizes
|
||||||
|
option.quality = quality;
|
||||||
|
if (!this.qualities.includes(option.quality)) {
|
||||||
|
this.qualities.push(quality);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const preferred = JSON.parse(this.qualityTarget.dataset.preferred) ?? [];
|
||||||
const preferred = this.qualityTarget.dataset.preferred;
|
this.qualityTarget.innerHTML = this.#serializeSelectOptions(this.qualities);
|
||||||
if (preferred) {
|
this.qualityTarget.tomselect.items = 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() {
|
async filter() {
|
||||||
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
|
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
|
||||||
|
|
||||||
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 ("tvshows" === this.mediaTypeValue) {
|
if ("tvshows" === this.mediaTypeValue) {
|
||||||
@@ -175,4 +134,16 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#fetchValuesFromNodeList(nodeList) {
|
||||||
|
return [...nodeList].map(option => option.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeSelectOptions(options) {
|
||||||
|
return this.defaultOptions + options.sort()
|
||||||
|
.map((option) => {
|
||||||
|
return '<option value="' + option + '">' + option + '</option>'
|
||||||
|
})
|
||||||
|
.join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -148,3 +158,34 @@ dialog[data-dialog-target="dialog"][closing] {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#filter {
|
||||||
|
.ts-wrapper {
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.ts-control {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item[data-ts-item] {
|
||||||
|
background-image: none !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
@apply bg-orange-500 rounded-ms font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@apply border-b-2 border-b-orange-600 bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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": {
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ use App\User\Database\QualityList;
|
|||||||
use App\User\Database\ResolutionList;
|
use App\User\Database\ResolutionList;
|
||||||
use App\User\Framework\Repository\PreferenceOptionRepository;
|
use App\User\Framework\Repository\PreferenceOptionRepository;
|
||||||
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;
|
||||||
@@ -38,6 +41,7 @@ class UserMediaPreferencesForm extends AbstractType
|
|||||||
'label_attr' => ['class' => 'w-64 text-white block font-semibold mb-2'],
|
'label_attr' => ['class' => 'w-64 text-white 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,70 +6,108 @@
|
|||||||
data-result-filter-tv-episode-list-outlet=".episode-list"
|
data-result-filter-tv-episode-list-outlet=".episode-list"
|
||||||
data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason"
|
data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason"
|
||||||
>
|
>
|
||||||
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
<div class="filter-items w-full p-4 flex flex-col md:flex-row gap-4 bg-black/20 border-2 border-orange-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
|
||||||
<label for="resolution">
|
<label for="resolution" class="filter-label">
|
||||||
Resolution
|
Resolution
|
||||||
<select id="resolution"
|
<select id="resolution"
|
||||||
|
multiple="multiple"
|
||||||
data-result-filter-target="resolution"
|
data-result-filter-target="resolution"
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
class="px-1 py-0.5 bg-stone-100 placeholder-text-gray-50 text-gray-50"
|
||||||
value="{{ app.user.userPreferenceValues["resolution"] }}"
|
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
|
||||||
|
create: false,
|
||||||
|
tomSelectOptions: {
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
>
|
>
|
||||||
<option value="">n/a</option>
|
<option value="">n/a</option>
|
||||||
{% for name, value in this.resolutionOptions %}
|
{% for name, value in this.resolutionOptions %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ value }}"
|
||||||
{{ value == this.userPreferences['resolution'] ? 'selected' }}
|
{{ value in this.userPreferences['resolution'] ? 'selected' }}
|
||||||
>{{ name }}</option>
|
>{{ name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label for="codec">
|
|
||||||
|
<label for="codec" class="filter-label">
|
||||||
Codec
|
Codec
|
||||||
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
|
<select id="codec"
|
||||||
|
multiple="multiple"
|
||||||
|
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
|
||||||
|
create: false,
|
||||||
|
tomSelectOptions: {
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-gray-50">
|
||||||
<option value="">n/a</option>
|
<option value="">n/a</option>
|
||||||
{% for name, value in this.codecOptions %}
|
{% for name, value in this.codecOptions %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ value }}"
|
||||||
{{ value == this.userPreferences['codec'] ? 'selected' }}
|
{{ value in this.userPreferences['codec'] ? 'selected' }}
|
||||||
>{{ name }}</option>
|
>{{ name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label for="language">
|
|
||||||
|
<label for="language" class="filter-label">
|
||||||
Language
|
Language
|
||||||
<select id="language"
|
<select id="language"
|
||||||
|
multiple="multiple"
|
||||||
data-result-filter-target="language"
|
data-result-filter-target="language"
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
|
||||||
|
create: false,
|
||||||
|
tomSelectOptions: {
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
class="px-1 py-0.5 bg-stone-100 text-gray-50"
|
||||||
{% if this.userPreferences['language'] != null %}
|
{% if this.userPreferences['language'] != null %}
|
||||||
data-preferred="{{ this.userPreferences['language'] }}"
|
data-preferred="{{ this.userPreferences['language']|json_encode }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label for="provider">
|
|
||||||
|
<label for="provider" class="filter-label">
|
||||||
Provider
|
Provider
|
||||||
<select id="provider"
|
<select id="provider"
|
||||||
|
multiple="multiple"
|
||||||
data-result-filter-target="provider"
|
data-result-filter-target="provider"
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
|
||||||
|
create: false,
|
||||||
|
tomSelectOptions: {
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
class="px-1 py-0.5 bg-stone-100 text-gray-50"
|
||||||
{% if this.userPreferences['provider'] != null %}
|
{% if this.userPreferences['provider'] != null %}
|
||||||
data-preferred="{{ this.userPreferences['provider'] }}"
|
data-preferred="{{ this.userPreferences['provider']|json_encode }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label for="quality">
|
|
||||||
|
<label for="quality" class="filter-label">
|
||||||
Quality
|
Quality
|
||||||
<select id="quality"
|
<select id="quality"
|
||||||
|
multiple="multiple"
|
||||||
data-result-filter-target="quality"
|
data-result-filter-target="quality"
|
||||||
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
|
class="px-1 py-0.5 bg-stone-100 text-gray-50"
|
||||||
{% if this.userPreferences['quality'] != null %}
|
data-preferred="{{ this.userPreferences['quality']|json_encode }}"
|
||||||
data-preferred="{{ this.userPreferences['quality'] }}"
|
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
|
||||||
{% endif %}
|
create: false,
|
||||||
|
tomSelectOptions: {
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
>
|
>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
<label for="season">
|
<label for="season" class="filter-label">
|
||||||
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"
|
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800"
|
||||||
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
|
||||||
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user