Compare commits

...

4 Commits

Author SHA1 Message Date
87e72ec55e fix: adds ImdbMatcher 2025-07-25 21:35:29 -05:00
23a88ec6bb fix: search by imdb id 2025-07-24 23:31:03 -05:00
d33a961f2d fix: codec filter 2025-07-24 17:09:30 -05:00
566886ef0e feat: uses web components to simplify javascript logic 2025-07-24 17:05:19 -05:00
15 changed files with 99 additions and 77 deletions

View File

@@ -40,15 +40,9 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
this.episodeId = this.getAttribute('episode-id') ?? null; this.episodeId = this.getAttribute('episode-id') ?? null;
this.#downloadBtnEl = this.querySelector('.download-btn'); this.#downloadBtnEl = this.querySelector('.download-btn');
this.#selectEpisodeInputEl = this.querySelector('input[type="checkbox"]'); this.#selectEpisodeInputEl = this.querySelector('input[type="checkbox"]');
console.log(this.#selectEpisodeInputEl)
this.#downloadBtnEl.addEventListener('click', () => this.download()); this.#downloadBtnEl.addEventListener('click', () => this.download());
// document.addEventListener('filterDownloadOptions', this.filter.bind(this));
} }
connectedCallback() {
}
get isSelected() { get isSelected() {
return this.#selectEpisodeInputEl.checked; return this.#selectEpisodeInputEl.checked;
} }
@@ -57,16 +51,6 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
this.#selectEpisodeInputEl.checked = value; this.#selectEpisodeInputEl.checked = value;
} }
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) return;
this[ property ] = newValue;
}
static get observedAttributes() {
return ['size', 'quality', 'resolution', 'codec', 'seeders', 'provider'];
}
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 = { const props = {
@@ -117,7 +101,6 @@ export default class DownloadOptionTr extends HTMLTableRowElement {
} }
download() { download() {
console.log('Downloading dis bihh')
fetch('/api/download', { fetch('/api/download', {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@@ -1,7 +1,4 @@
export default class EpisodeContainer extends HTMLElement { export default class EpisodeContainer extends HTMLElement {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
options = []; options = [];
showTitle; showTitle;
@@ -27,19 +24,6 @@ export default class EpisodeContainer extends HTMLElement {
document.addEventListener('downloadSelectedEpisodes', this.downloadSelectedResults.bind(this)); document.addEventListener('downloadSelectedEpisodes', this.downloadSelectedResults.bind(this));
document.addEventListener('selectEpisodeForDownload', (e) => this.selectEpisodeForDownload(e.detail.select)); document.addEventListener('selectEpisodeForDownload', (e) => this.selectEpisodeForDownload(e.detail.select));
} }
connectedCallback() {
}
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) return;
this[ property ] = newValue;
}
static get observedAttributes() {
return ['name'];
}
toggleResults() { toggleResults() {
this.#resultsToggleBtnEl.classList.toggle('rotate-90'); this.#resultsToggleBtnEl.classList.toggle('rotate-90');

View File

@@ -1,7 +1,4 @@
export default class MovieContainer extends HTMLElement { export default class MovieContainer extends HTMLElement {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
#resultsTableEl; #resultsTableEl;
#resultsCountNumberEl; #resultsCountNumberEl;
@@ -13,12 +10,6 @@ export default class MovieContainer extends HTMLElement {
document.addEventListener('filterDownloadOptions', this.filter.bind(this)); document.addEventListener('filterDownloadOptions', this.filter.bind(this));
} }
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) return;
this[ property ] = newValue;
}
filter({ detail: { activeFilter } }) { filter({ detail: { activeFilter } }) {
const options = this.querySelectorAll('tr.download-option'); const options = this.querySelectorAll('tr.download-option');
let firstIncluded = true; let firstIncluded = true;

View File

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

View File

@@ -19,7 +19,7 @@ export default class extends Controller {
"quality": "", "quality": "",
} }
static outlets = ['movie-results', 'tv-results', '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 = {
'imdbId': String, 'imdbId': String,
@@ -55,6 +55,10 @@ export default class extends Controller {
})); }));
} }
downloadSelectedEpisodes() {
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
}
addLanguages(option) { addLanguages(option) {
const languages = Object.assign([], option.languages); const languages = Object.assign([], option.languages);
languages.forEach((language) => { languages.forEach((language) => {
@@ -171,8 +175,4 @@ export default class extends Controller {
} }
}) })
} }
downloadSelectedEpisodes() {
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
}
} }

View File

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

View File

@@ -18,7 +18,7 @@ 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 = []
@@ -37,17 +37,4 @@ export default class extends Controller {
this.episodeSelectorTarget.disabled = true; this.episodeSelectorTarget.disabled = true;
} }
} }
download() {
this.element.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;
}
})
}
} }

View 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);
}
}

View File

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

View 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,
) {}
}

View File

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

View File

@@ -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",
];
}
} }
} }

View File

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

View File

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

View File

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