wip: filters movie results, adds options to filter input

This commit is contained in:
2025-07-06 15:37:29 -05:00
parent 402d513147
commit e54bcd44d8
8 changed files with 85 additions and 80 deletions

View File

@@ -52,6 +52,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

@@ -11,6 +11,7 @@ export default class extends Controller {
languages = [] languages = []
providers = [] providers = []
qualities = []
seasons = [] seasons = []
activeFilter = { activeFilter = {
@@ -18,10 +19,11 @@ 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,
@@ -49,6 +51,7 @@ export default class extends Controller {
outlet.options.forEach((option) => { 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 +108,30 @@ export default class extends Controller {
} }
addQualities(option, props) {
if (!this.qualities.includes(props['quality'])) {
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 +141,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

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

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

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

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