Compare commits

..

5 Commits

22 changed files with 251 additions and 107 deletions

View File

@@ -26,12 +26,7 @@ export default class extends Controller {
// this.fooTarget.addEventListener('click', this._fooBar) // this.fooTarget.addEventListener('click', this._fooBar)
} }
navbarOutletConnected(outlet) {
console.log(outlet)
}
toggleMenu() { toggleMenu() {
console.log(this.navbarOutlet);
this.navbarOutlet.toggle(); this.navbarOutlet.toggle();
} }

View File

@@ -16,6 +16,7 @@ export default class extends Controller {
}; };
static targets = ['list'] static targets = ['list']
static outlets = ['loading-icon']
options = [] options = []
optionsLoaded = false optionsLoaded = false
@@ -33,6 +34,8 @@ export default class extends Controller {
this.element.innerHTML = response; this.element.innerHTML = response;
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();
}); });
} }
} }
@@ -52,6 +55,7 @@ export default class extends Controller {
"resolution": option.querySelector('#resolution').textContent.trim(), "resolution": option.querySelector('#resolution').textContent.trim(),
"codec": option.querySelector('#codec').textContent.trim(), "codec": option.querySelector('#codec').textContent.trim(),
"provider": option.querySelector('#provider').textContent.trim(), "provider": option.querySelector('#provider').textContent.trim(),
"quality": option.dataset['quality'],
"languages": JSON.parse(option.dataset['languages']), "languages": JSON.parse(option.dataset['languages']),
} }

View File

@@ -10,8 +10,7 @@ export default class extends Controller {
activeStyles = "block rounded-lg bg-orange-500 hover:bg-opacity-70 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-80 px-4 py-2 text-sm font-medium text-gray-50"; activeStyles = "block rounded-lg bg-orange-500 hover:bg-opacity-70 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-80 px-4 py-2 text-sm font-medium text-gray-50";
connect() { connect() {
console.log(window.location.pathname); this.element.querySelectorAll('.nav-list a:not(.nav-foot)').forEach(link => {
this.element.querySelectorAll('a:not(.nav-foot)').forEach(link => {
link.className = this.inactiveStyles; link.className = this.inactiveStyles;
if (window.location.pathname === (new URL(link.href)).pathname || window.location.pathname.startsWith( (new URL(link.href)).pathname + '/' ) ) { if (window.location.pathname === (new URL(link.href)).pathname || window.location.pathname.startsWith( (new URL(link.href)).pathname + '/' ) ) {
link.className = this.activeStyles; link.className = this.activeStyles;

View File

@@ -11,6 +11,7 @@ export default class extends Controller {
languages = [] languages = []
providers = [] providers = []
qualities = []
seasons = [] seasons = []
activeFilter = { activeFilter = {
@@ -18,13 +19,15 @@ export default class extends Controller {
"codec": "", "codec": "",
"language": "", "language": "",
"provider": "", "provider": "",
"quality": "",
} }
static outlets = ['movie-results', 'tv-results', 'tv-episode-list'] static outlets = ['movie-results', 'tv-results', 'tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'selectAll', 'downloadSelected'] static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected']
static values = { static values = {
'media-type': String, 'media-type': String,
'episodes': Array, 'episodes': Array,
'reverseMappedQualities': Object,
} }
async connect() { async connect() {
@@ -34,21 +37,12 @@ export default class extends Controller {
await this.filter(); await this.filter();
} }
async movieResultsOutletConnected(outlet) { // Event is fired from movies/tvshows controllers to populate this data
await this.parseDownloadOptionForFilter(outlet) async loadOptions({detail: { options }}) {
} await options.forEach((option) => {
async tvResultsOutletConnected(outlet) {
await this.parseDownloadOptionForFilter(outlet)
}
async parseDownloadOptionForFilter(outlet) {
if (outlet.options.length === 0) {
await outlet.setOptions();
}
outlet.options.forEach((option) => {
this.addLanguages(option, option.dataset); this.addLanguages(option, option.dataset);
this.addProviders(option, option.dataset); this.addProviders(option, option.dataset);
this.addQualities(option, option.dataset);
}) })
await this.filter(); await this.filter();
} }
@@ -105,6 +99,32 @@ export default class extends Controller {
} }
addQualities(option, props) {
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() { async filter() {
const currentSeason = this.activeFilter['season']; const currentSeason = this.activeFilter['season'];
@@ -114,6 +134,7 @@ export default class extends Controller {
"codec": this.codecTarget.value, "codec": this.codecTarget.value,
"language": this.languageTarget.value, "language": this.languageTarget.value,
"provider": this.providerTarget.value, "provider": this.providerTarget.value,
"quality": this.qualityTarget.value,
} }
if ("movies" === this.mediaTypeValue) { if ("movies" === this.mediaTypeValue) {

View File

@@ -51,6 +51,7 @@ export default class extends Controller {
this.countTarget.innerText = 0; this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true; this.episodeSelectorTarget.disabled = true;
} }
this.dispatch('optionsLoaded', {detail: {options: this.options}})
this.loadingIconOutlet.increaseCount(); this.loadingIconOutlet.increaseCount();
} else { } else {
console.log(`HTTP Response Code: ${response?.status}`) console.log(`HTTP Response Code: ${response?.status}`)

View File

@@ -2,8 +2,10 @@
namespace App\Command; namespace App\Command;
use App\User\Framework\Entity\UserPreference;
use App\User\Framework\Repository\PreferenceOptionRepository; use App\User\Framework\Repository\PreferenceOptionRepository;
use App\User\Framework\Repository\PreferencesRepository; use App\User\Framework\Repository\PreferencesRepository;
use App\User\Framework\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@@ -18,14 +20,17 @@ class SeedDatabaseCommand extends Command
{ {
private PreferencesRepository $preferenceRepository; private PreferencesRepository $preferenceRepository;
private PreferenceOptionRepository $preferenceOptionRepository; private PreferenceOptionRepository $preferenceOptionRepository;
private UserRepository $userRepository;
public function __construct( public function __construct(
PreferencesRepository $preferenceRepository, PreferencesRepository $preferenceRepository,
PreferenceOptionRepository $preferenceOptionRepository, PreferenceOptionRepository $preferenceOptionRepository,
UserRepository $userRepository,
) { ) {
parent::__construct(); parent::__construct();
$this->preferenceRepository = $preferenceRepository; $this->preferenceRepository = $preferenceRepository;
$this->preferenceOptionRepository = $preferenceOptionRepository; $this->preferenceOptionRepository = $preferenceOptionRepository;
$this->userRepository = $userRepository;
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
@@ -34,6 +39,7 @@ class SeedDatabaseCommand extends Command
$this->seedPreferences($io); $this->seedPreferences($io);
$this->seedPreferenceOptions($io); $this->seedPreferenceOptions($io);
$this->updateUserPreferences($io);
return Command::SUCCESS; return Command::SUCCESS;
} }
@@ -60,6 +66,26 @@ class SeedDatabaseCommand extends Command
$this->preferenceRepository->getEntityManager()->flush(); $this->preferenceRepository->getEntityManager()->flush();
} }
private function updateUserPreferences(SymfonyStyle $io)
{
$io->info('[SeedDatabaseCommand] > Updating user preferences...');
$users = $this->userRepository->findAll();
$preferences = $this->preferenceRepository->findAll();
foreach ($users as $user) {
foreach ($preferences as $preference) {
if (false === $user->hasUserPreference($preference->getId())) {
$user->addUserPreference(
new UserPreference()
->setPreference($preference)
->setUser($user)
->setPreferenceValue(null)
);
}
}
}
$this->userRepository->getEntityManager()->flush();
}
private function getPreferences(): array private function getPreferences(): array
{ {
return [ return [
@@ -91,6 +117,13 @@ class SeedDatabaseCommand extends Command
'enabled' => true, 'enabled' => true,
'type' => 'media', 'type' => 'media',
], ],
[
'id' => 'quality',
'name' => 'Quality',
'description' => null,
'enabled' => true,
'type' => 'media',
],
[ [
'id' => 'movie_folder', 'id' => 'movie_folder',
'name' => 'Create new folder for Movies', 'name' => 'Create new folder for Movies',

View File

@@ -8,6 +8,7 @@ use App\Monitor\Action\Handler\MonitorTvShowHandler;
use App\Monitor\Framework\Scheduler\MonitorDispatcher; use App\Monitor\Framework\Scheduler\MonitorDispatcher;
use App\Tmdb\Tmdb; use App\Tmdb\Tmdb;
use App\Tmdb\TmdbResult; use App\Tmdb\TmdbResult;
use App\User\Framework\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -23,6 +24,8 @@ final class IndexController extends AbstractController
#[Route('/', name: 'app_index')] #[Route('/', name: 'app_index')]
public function index(Request $request): Response public function index(Request $request): Response
{ {
/** @var User $user */
$user = $this->getUser();
return $this->render('index/index.html.twig', [ return $this->render('index/index.html.twig', [
'active_downloads' => $this->getUser()->getActiveDownloads(), 'active_downloads' => $this->getUser()->getActiveDownloads(),
'recent_downloads' => $this->getUser()->getDownloads(), 'recent_downloads' => $this->getUser()->getDownloads(),

View File

@@ -20,9 +20,6 @@ use Symfony\Contracts\Cache\ItemInterface;
final class ApiController extends AbstractController final class ApiController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly GetMovieOptionsHandler $getMovieOptionsHandler,
private readonly GetTvShowOptionsHandler $getTvShowOptionsHandler,
private readonly Broadcaster $broadcaster,
private readonly Torrentio $torrentio, private readonly Torrentio $torrentio,
) {} ) {}
@@ -38,79 +35,4 @@ final class ApiController extends AbstractController
$this->torrentio->search($imdbId, 'movies', false), $this->torrentio->search($imdbId, 'movies', false),
); );
} }
#[Route('/torrentio/movies/{tmdbId}/{imdbId}', name: 'app_torrentio_movies')]
public function movieOptions(GetMovieOptionsInput $input, CacheInterface $cache): Response
{
$cacheId = sprintf(
"page.torrentio.movies.%s.%s",
$input->tmdbId,
$input->imdbId
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getMovieOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/movies.html.twig', [
'results' => $results,
]);
});
}
#[Route('/torrentio/tvshows/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_torrentio_tvshows')]
public function tvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
$input->tmdbId,
$input->imdbId,
$input->season,
$input->episode,
);
try {
// return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
// $item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/tvshows.html.twig', [
'results' => $results,
]);
// });
} catch (TorrentioRateLimitException $exception) {
$this->broadcaster->alert('Warning', 'Torrentio has rate limited your requests. Please wait a few minutes before trying again.', 'warning');
return $this->render('bare.html.twig',
[],
new Response('Too many requests',
Response::HTTP_TOO_MANY_REQUESTS,
['Retry-After' => 4000]
)
);
}
}
#[Route('/torrentio/tvshows/clear/{tmdbId}/{imdbId}/{season?}/{episode?}', name: 'app_clear_torrentio_tvshows')]
public function clearTvShowOptions(GetTvShowOptionsInput $input, CacheInterface $cache, Request $request): Response
{
$cacheId = sprintf(
"page.torrentio.tvshows.%s.%s.%s.%s",
$input->tmdbId,
$input->imdbId,
$input->season,
$input->episode,
);
$cache->delete($cacheId);
$this->broadcaster->alert(
title: 'Success',
message: 'Torrentio cache Cleared.'
);
return $cache->get($cacheId, function (ItemInterface $item) use ($input) {
$item->expiresAt(Carbon::now()->addHour()->setMinute(0)->setSecond(0));
$results = $this->getTvShowOptionsHandler->handle($input->toCommand());
return $this->render('torrentio/tvshows.html.twig', [
'results' => $results,
]);
});
}
} }

View File

@@ -22,6 +22,7 @@ class ResultFactory
string $bingeGroup = "-" string $bingeGroup = "-"
) { ) {
$ptn = (object) (new PTN())->parse($title); $ptn = (object) (new PTN())->parse($title);
// dump($ptn);
return new TorrentioResult( return new TorrentioResult(
self::trimTitle($title), self::trimTitle($title),
urldecode($url), urldecode($url),
@@ -34,6 +35,7 @@ class ResultFactory
$bingeGroup, $bingeGroup,
$ptn->resolution ?? "-", $ptn->resolution ?? "-",
self::setCodec($ptn->codec ?? "-"), self::setCodec($ptn->codec ?? "-"),
$ptn->quality ?? "-",
$ptn, $ptn,
substr(base64_encode($url), strlen($url) - 10), substr(base64_encode($url), strlen($url) - 10),
$ptn->episode ?? "-", $ptn->episode ?? "-",

View File

@@ -16,6 +16,7 @@ class TorrentioResult
public ?string $bingeGroup = "-", public ?string $bingeGroup = "-",
public ?string $resolution = "-", public ?string $resolution = "-",
public ?string $codec = "-", public ?string $codec = "-",
public ?string $quality = "-",
public object|array $ptn = [], public object|array $ptn = [],
public ?string $key = "-", public ?string $key = "-",
public ?string $episodeNumber = "-", public ?string $episodeNumber = "-",

View File

@@ -4,6 +4,7 @@ namespace App\Twig\Components;
use Aimeos\Map; use Aimeos\Map;
use App\User\Framework\Repository\PreferencesRepository; use App\User\Framework\Repository\PreferencesRepository;
use App\Util\QualityList;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\DefaultActionTrait;
@@ -17,6 +18,8 @@ final class Filter
public array $userPreferences = []; public array $userPreferences = [];
public array $reverseMappedQualities = [];
public function __construct( public function __construct(
private readonly PreferencesRepository $preferencesRepository, private readonly PreferencesRepository $preferencesRepository,
private readonly Security $security, private readonly Security $security,
@@ -27,5 +30,6 @@ final class Filter
->toArray(); ->toArray();
$this->userPreferences = Map::from($this->security->getUser()->getUserPreferenceValues()) $this->userPreferences = Map::from($this->security->getUser()->getUserPreferenceValues())
->toArray(); ->toArray();
$this->reverseMappedQualities = QualityList::getAsReverseMap();
} }
} }

View File

@@ -10,6 +10,7 @@ class SaveUserMediaPreferencesCommand implements CommandInterface
public function __construct( public function __construct(
public string $resolution, public string $resolution,
public string $codec, public string $codec,
public string $quality,
public string $language, public string $language,
public string $provider, public string $provider,
) {} ) {}

View File

@@ -18,6 +18,9 @@ class SaveUserMediaPreferencesInput implements InputInterface
#[SourceRequest('resolution')] #[SourceRequest('resolution')]
public string $resolution, public string $resolution,
#[SourceRequest('quality')]
public string $quality,
#[SourceRequest('codec')] #[SourceRequest('codec')]
public string $codec, public string $codec,
@@ -33,6 +36,7 @@ class SaveUserMediaPreferencesInput implements InputInterface
return new SaveUserMediaPreferencesCommand( return new SaveUserMediaPreferencesCommand(
$this->resolution, $this->resolution,
$this->codec, $this->codec,
$this->quality,
$this->language, $this->language,
$this->provider, $this->provider,
); );

View File

@@ -12,6 +12,7 @@ use App\User\Framework\Repository\PreferencesRepository;
use App\Util\Broadcaster; use App\Util\Broadcaster;
use App\Util\CountryLanguages; use App\Util\CountryLanguages;
use App\Util\ProviderList; use App\Util\ProviderList;
use App\Util\QualityList;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -36,7 +37,8 @@ class PreferencesController extends AbstractController
[ [
'preferences' => $this->preferencesRepository->findEnabled(), 'preferences' => $this->preferencesRepository->findEnabled(),
'languages' => $languages, 'languages' => $languages,
'providers' => ProviderList::$providers, 'providers' => ProviderList::getProviders(),
'qualities' => QualityList::getBaseQualities(),
'mediaPreferences' => $mediaPreferences, 'mediaPreferences' => $mediaPreferences,
'downloadPreferences' => $downloadPreferences, 'downloadPreferences' => $downloadPreferences,
] ]
@@ -67,6 +69,7 @@ class PreferencesController extends AbstractController
'preferences' => $this->preferencesRepository->findEnabled(), 'preferences' => $this->preferencesRepository->findEnabled(),
'languages' => $languages, 'languages' => $languages,
'providers' => ProviderList::$providers, 'providers' => ProviderList::$providers,
'qualities' => QualityList::getBaseQualities(),
'mediaPreferences' => $mediaPreferences, 'mediaPreferences' => $mediaPreferences,
'downloadPreferences' => $downloadPreferences, 'downloadPreferences' => $downloadPreferences,
] ]

View File

@@ -209,7 +209,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return Map::from($this->userPreferences) return Map::from($this->userPreferences)
->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId()) ->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId())
->map(function (UserPreference $userPreference) { ->map(function (UserPreference $userPreference) {
if (in_array($userPreference->getPreference()->getId(), ['language', 'provider'])) { if (in_array($userPreference->getPreference()->getId(), ['language', 'provider', 'quality'])) {
return $userPreference->getPreferenceValue(); return $userPreference->getPreferenceValue();
} }
foreach ($userPreference->getPreference()->getPreferenceOptions() as $preferenceOption) { foreach ($userPreference->getPreference()->getPreferenceOptions() as $preferenceOption) {

115
src/Util/QualityList.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
namespace App\Util;
class QualityList
{
public static $qualities = [
"dvd-rip" => [
"dvdrip",
"dvdmux",
"dvdr",
"dvd-full",
"full-rip",
"iso rip",
"lossless rip",
"untouched rip",
"dvd-5",
"dvd-9",
],
"hdtv, pdtv or dsrip" => [
"dsr",
"dsrip",
"satrip",
"dthrip",
"dvbrip",
"hdtv",
"pdtv",
"dtvrip",
"tvrip",
"hdtvrip",
],
"vodrip" => [
"vodrip",
"vodr",
],
"hc hd-rip" => [
"hc",
"hd-rip",
],
"webcap" => [
"web-cap",
"webcap",
"web cap",
],
"hdrip" => [
"hdrip",
"web-dlrip",
],
"webrip" => [
"webrip",
"web rip",
"web-rip",
"webrip (p2p)",
"web rip (p2p)",
"web-rip (p2p)",
],
"web-dl" => [
"webdl",
"web dl",
"web-dl",
"web (scene)",
"webrip",
],
"blu-ray/bd/brrip" => [
"blu-ray",
"bluray",
"bluray",
"bdrip",
"brip",
"brrip",
"bdr[13]",
"bd25",
"bd50",
"bd66",
"bd100",
"bd5",
"bd9",
"bdmv",
"bdiso",
"complete.bluray",
],
"4k" => [
"cbr",
"vbr",
],
];
public static function getQualities(): array
{
return self::$qualities;
}
public static function getBaseQualities(): array
{
return array_keys(self::$qualities);
}
public static function getBaseQualityFromSubQuality(string $key): ?string
{
return array_search($key, self::$qualities) ?? null;
}
public static function getAsReverseMap(): array
{
$results = [];
foreach (self::$qualities as $baseQualtiy => $subQualities) {
foreach ($subQualities as $subQuality) {
$results[$subQuality] = $baseQualtiy;
}
}
return $results;
}
}

View File

@@ -1,10 +1,10 @@
<div id="filter" class="flex flex-col gap-4" <div id="filter" class="flex flex-col gap-4"
{{ stimulus_controller('result_filter') }} {{ stimulus_controller('result_filter', {reverseMappedQualities: this.reverseMappedQualities}) }}
{{ stimulus_action('result_filter', 'filter', 'change') }}
data-result-filter-media-type-value="{{ results.media.mediaType }}" data-result-filter-media-type-value="{{ results.media.mediaType }}"
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"
> >
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg"> <div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
<label for="resolution"> <label for="resolution">
@@ -55,6 +55,17 @@
> >
</select> </select>
</label> </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" %} {% if results.media.mediaType == "tvshows" %}
<label for="season"> <label for="season">
Season Season

View File

@@ -1,6 +1,7 @@
<header {{ attributes }} class="bg-cyan-950 z-40"> <header {{ attributes }} class="bg-cyan-950 z-40">
<div class="px-4 sm:px-6 lg:px-8"> <div class="px-4 sm:px-6 lg:px-8">
<div class="h-16 flex flex-row items-center justify-between"> <div class="h-16 flex flex-row items-center justify-between">
<a href="{{ path('app_index') }}" class="text-2xl text-orange-500 mr-4 md:hidden">T</a>
<twig:SearchBar /> <twig:SearchBar />
<div class="md:flex md:items-center md:gap-12"> <div class="md:flex md:items-center md:gap-12">
<nav aria-label="Global" class="md:block"> <nav aria-label="Global" class="md:block">

View File

@@ -1,7 +1,7 @@
<nav id="navbar" {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950 animate__animated animate__slideInLeft animate__slow"> <nav id="navbar" {{ attributes }} {{ stimulus_controller('navbar') }} {{ stimulus_action('navbar', 'setActive')}} class="flex h-screen flex-col justify-between bg-cyan-950 animate__animated animate__slideInLeft animate__slow">
<div class="px-4 py-4 flex flex-col gap-12"> <div class="px-4 py-4 flex flex-col gap-12">
<h1 class="text-3xl font-extrabold text-orange-500 mb-3">Torsearch</h1> <h1 class="text-3xl mt-12 md:mt-0 font-extrabold text-orange-500 mb-3"><a href="{{ path('app_index') }}">Torsearch</a></h1>
<ul class="space-y-1"> <ul class="nav-list space-y-1">
<li> <li>
<a href="{{ path('app_index') }}" <a href="{{ path('app_index') }}"
class="block rounded-lg class="block rounded-lg

View File

@@ -115,7 +115,10 @@
<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" {{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}> <div class="results"
{{ stimulus_controller('movie_results', {title: results.media.title, tmdbId: results.media.tmdbId, imdbId: results.media.imdbId}) }}
data-movie-results-loading-icon-outlet=".loading-icon"
>
</div> </div>
{% elseif "tvshows" == results.media.mediaType %} {% elseif "tvshows" == results.media.mediaType %}
<twig:TvEpisodeList <twig:TvEpisodeList

View File

@@ -8,6 +8,10 @@
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white"> class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
Size Size
</th> </th>
<th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
Quality
</th>
<th scope="col" <th scope="col"
class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white"> class="px-4 py-4 leading-[20px] font-medium text-gray-900 whitespace-nowrap dark:text-white">
Resolution Resolution
@@ -37,10 +41,13 @@
</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-provider="{{ result.provider }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% endif %}> <tr class="bg-white dark:bg-slate-700 flex flex-col flex-no wrap r-tablerow border-b border-gray-500" data-provider="{{ result.provider }}" data-quality="{{ result.quality }}" data-languages="{{ result.languages|json_encode }}" {% if "tvshows" == results.media.mediaType %} data-season="{{ results.season }}"{% 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>
<td id="quality" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.quality }}
</td>
<td id="resolution" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50"> <td id="resolution" class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-50">
{{ result.resolution }} {{ result.resolution }}
</td> </td>

View File

@@ -7,6 +7,20 @@
<twig:Card title="Media Preferences" class="w-full"> <twig:Card title="Media Preferences" class="w-full">
<p class="text-gray-50 mb-2">Define a filter to be pre-applied to your download options.</p> <p class="text-gray-50 mb-2">Define a filter to be pre-applied to your download options.</p>
<form id="media_preferences" class="flex flex-col max-w-64" name="media_preferences" method="post" action="{{ path('app_save_media_preferences') }}"> <form id="media_preferences" class="flex flex-col max-w-64" name="media_preferences" method="post" action="{{ path('app_save_media_preferences') }}">
<label class="text-gray-50" for="quality">Quality</label>
<select class="p-1.5 rounded-md mb-2" name="quality" id="quality" value="{{ mediaPreferences['quality'].getPreferenceValue() }}">
<option class="text-gray-800"
value=""
{{ mediaPreferences['quality'].getPreferenceValue() is null ? "selected" }}
>n/a</option>
{% for quality in qualities %}
<option class="text-gray-800"
value="{{ quality }}"
{{ quality == mediaPreferences['quality'].getPreferenceValue() ? "selected" }}
>{{ quality }}</option>
{% endfor %}
</select>
<label class="text-gray-50" for="resolution">Resolution</label> <label class="text-gray-50" for="resolution">Resolution</label>
<select class="p-1.5 rounded-md mb-2" name="resolution" id="resolution" value="{{ mediaPreferences['resolution'].getPreferenceValue() }}"> <select class="p-1.5 rounded-md mb-2" name="resolution" id="resolution" value="{{ mediaPreferences['resolution'].getPreferenceValue() }}">
<option class="text-gray-800" <option class="text-gray-800"