Compare commits

...

4 Commits

10 changed files with 115 additions and 132 deletions

View File

@@ -41,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();
} }
} }
} }

View File

@@ -12,14 +12,14 @@ export default class extends Controller {
seasons = [] seasons = []
activeFilter = { activeFilter = {
"resolution": "", "resolution": [],
"codec": "", "codec": [],
"language": "", "language": [],
"provider": "", "provider": [],
"quality": "", "quality": [],
} }
defaultOptions = '<option value="">n/a</option><option value="-">-</option>'; defaultOptions = '<option value="-">-</option>';
static outlets = ['tv-episode-list'] static outlets = ['tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected'] static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected']
@@ -31,13 +31,21 @@ 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;
} }
this.filter();
this.setTimerToStopLoadingIcon();
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
} }
setTimerToStopLoadingIcon() { setTimerToStopLoadingIcon() {
@@ -45,13 +53,10 @@ export default class extends Controller {
} }
// Event is fired from movies/tvshows controllers to populate this data // Event is fired from movies/tvshows controllers to populate this data
loadOptions({detail: { options }}) { async loadOptions({detail: { options }}) {
options.forEach((option) => { await options.forEach((option) => {
this.addLanguages(option); option.filter({detail: {activeFilter: this.activeFilter }});
this.addProviders(option);
this.addQualities(option);
}) })
this.filter();
} }
selectAllEpisodes() { selectAllEpisodes() {
@@ -66,41 +71,6 @@ export default class extends Controller {
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {})); document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
} }
addLanguages(option) {
option.languages.forEach((language) => {
if (!this.languages.includes(language)) {
this.languages.push(language);
}
});
const preferred = JSON.parse(this.languageTarget.dataset.preferred) ?? [];
this.languageTarget.innerHTML = this.#serializeSelectOptions(this.languages);
this.languageTarget.tomselect.items = preferred;
}
addProviders(option) {
if (!this.providers.includes(option.provider)) {
this.providers.push(option.provider);
}
const preferred = JSON.parse(this.providerTarget.dataset.preferred) ?? [];
this.providerTarget.innerHTML = this.#serializeSelectOptions(this.providers);
this.providerTarget.tomselect.items = preferred;
}
addQualities(option) {
if (option.quality.toLowerCase() in this.reverseMappedQualitiesValue) {
let quality = this.reverseMappedQualitiesValue[option.quality.toLowerCase()];
// 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) ?? [];
this.qualityTarget.innerHTML = this.#serializeSelectOptions(this.qualities);
this.qualityTarget.tomselect.items = preferred;
}
filter() { filter() {
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal"); const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");

View File

@@ -30,11 +30,11 @@ export default class extends Controller {
option.querySelector('.download-btn').dataset['title'] = this.titleValue option.querySelector('.download-btn').dataset['title'] = this.titleValue
); );
this.element.options[0].querySelector('input[type="checkbox"]').checked = true; this.element.options[0].querySelector('input[type="checkbox"]').checked = true;
this.loadingIconOutlet.increaseCount();
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}})); document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}}));
} else { } else {
this.countTarget.innerText = 0; this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true; this.episodeSelectorTarget.disabled = true;
} }
this.loadingIconOutlet.increaseCount();
} }
} }

View File

@@ -165,9 +165,9 @@ dialog[data-dialog-target="dialog"][closing] {
padding: 0; padding: 0;
.ts-control { .ts-control {
background: transparent !important;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
@apply bg-orange-500/60 backdrop-filter backdrop-blur-md;
} }
.item[data-ts-item] { .item[data-ts-item] {
@@ -175,10 +175,10 @@ dialog[data-dialog-target="dialog"][closing] {
border: none; border: none;
box-shadow: none; box-shadow: none;
text-shadow: none; text-shadow: none;
@apply bg-orange-500 rounded-ms font-bold; @apply bg-orange-500 rounded-ms font-bold text-black;
} }
@apply border-b-2 border-b-orange-600 bg-transparent; @apply border border-orange-500 bg-transparent rounded-ms;
} }
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove { .ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {

View File

@@ -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;
}
} }

View File

@@ -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 {

View File

@@ -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())
]);
}
}

View File

@@ -53,9 +53,9 @@ class UserMediaPreferencesForm extends AbstractType
'data-preferred' => \json_encode($this->userPreferences->$fieldName), 'data-preferred' => \json_encode($this->userPreferences->$fieldName),
], ],
'row_attr' => [ 'row_attr' => [
'class' => 'filter-label' 'class' => 'filter-label text-white'
], ],
'label_attr' => ['class' => 'text-white block font-semibold mb-2'], 'label_attr' => ['class' => 'block font-semibold mb-2'],
'choices' => $this->addDefaultChoice($choices), 'choices' => $this->addDefaultChoice($choices),
'required' => false, 'required' => false,
'multiple' => true, 'multiple' => true,
@@ -68,13 +68,13 @@ 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' => [ 'attr' => [
'class' => 'filter-items w-full p-4 bg-black/20 border-2 border-orange-500 text-md text-gray-500 dark:text-gray-50 rounded-lg', '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;
} }
} }

View File

@@ -6,9 +6,11 @@
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"
> >
{% set preferences_form = form %} {% set preferences_form = form %}
{{ form_start(preferences_form) }} {{ form_start(preferences_form) }}
<div class="flex flex-col md:flex-row gap-2 justify-between"> <h3 class="font-bold text-lg mb-2 md:mb-4">Apply a filter to your results</h3>
<div class="flex flex-col md:flex-row gap-2 justify-between">
{{ 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) }} {{ form_row(preferences_form.language) }}
@@ -20,7 +22,7 @@
<label for="season"> <label for="season">
Season Season
</label> </label>
<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 text-white rounded-ms" <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"
{{ stimulus_action('result_filter', 'setSeason', 'change') }} {{ stimulus_action('result_filter', 'setSeason', 'change') }}
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }} {{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
> >

View File

@@ -16,7 +16,7 @@
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'] }}" />