Merge pull request #364 from castdrian/feat-urls-quicksearch

feat: human readable urls & quicksearch
This commit is contained in:
mrjvs 2023-06-29 21:40:56 +02:00 committed by GitHub
commit 2fa44b8f51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 51 deletions

View File

@ -32,6 +32,7 @@
"react-stickynode": "^4.1.0",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.0",
"slugify": "^1.6.6",
"subsrt-ts": "^2.1.1",
"unpacker": "^1.0.1"
},

View File

@ -2,6 +2,7 @@ import { FetchError } from "ofetch";
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
import {
TMDBIdToUrlId,
TMDBMediaToMediaType,
formatTMDBMeta,
getEpisodes,
@ -12,7 +13,7 @@ import {
mediaTypeToTMDB,
} from "./tmdb";
import {
JWMediaResult,
JWDetailedMeta,
JWSeasonMetaResult,
JW_API_BASE,
} from "./types/justwatch";
@ -25,23 +26,6 @@ import {
} from "./types/tmdb";
import { makeUrl, proxiedFetch } from "../helpers/fetch";
type JWExternalIdType =
| "eidr"
| "imdb_latest"
| "imdb"
| "tmdb_latest"
| "tmdb"
| "tms";
interface JWExternalId {
provider: JWExternalIdType;
external_id: string;
}
interface JWDetailedMeta extends JWMediaResult {
external_ids: JWExternalId[];
}
export interface DetailedMeta {
meta: MWMediaMeta;
imdbId?: string;
@ -180,27 +164,6 @@ export async function getLegacyMetaFromId(
};
}
export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
}
export function decodeTMDBId(
paramId: string
): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "tmdb") return null;
let mediaType;
try {
mediaType = TMDBMediaToMediaType(type);
} catch {
return null;
}
return {
type: mediaType,
id,
};
}
export function isLegacyUrl(url: string): boolean {
if (url.startsWith("/media/JW")) return true;
return false;
@ -224,10 +187,12 @@ export async function convertLegacyUrl(
// movies always have an imdb id on tmdb
if (imdbId && mediaType === MWMediaType.MOVIE) {
const movieId = await getMovieFromExternalId(imdbId);
if (movieId) return `/media/tmdb-movie-${movieId}`;
}
if (movieId) {
return `/media/${TMDBIdToUrlId(mediaType, movieId, meta.meta.title)}`;
}
if (tmdbId) {
return `/media/tmdb-${type}-${tmdbId}`;
if (tmdbId) {
return `/media/${TMDBIdToUrlId(mediaType, tmdbId, meta.meta.title)}`;
}
}
}

View File

@ -1,3 +1,5 @@
import slugify from "slugify";
import { conf } from "@/setup/config";
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
@ -11,12 +13,15 @@ import {
TMDBMovieExternalIds,
TMDBMovieResponse,
TMDBMovieResult,
TMDBMovieSearchResult,
TMDBSearchResult,
TMDBSeason,
TMDBSeasonMetaResult,
TMDBShowData,
TMDBShowExternalIds,
TMDBShowResponse,
TMDBShowResult,
TMDBShowSearchResult,
} from "./types/tmdb";
import { mwFetch } from "../helpers/fetch";
@ -74,8 +79,21 @@ export function formatTMDBMeta(
};
}
export function TMDBIdToUrlId(
type: MWMediaType,
tmdbId: string,
title: string
) {
return [
"tmdb",
mediaTypeToTMDB(type),
tmdbId,
slugify(title, { lower: true, strict: true }),
].join("-");
}
export function TMDBMediaToId(media: MWMediaMeta): string {
return ["tmdb", mediaTypeToTMDB(media.type), media.id].join("-");
return TMDBIdToUrlId(media.type, media.id, media.title);
}
export function decodeTMDBId(
@ -143,6 +161,38 @@ export async function searchMedia(
return data;
}
export async function multiSearch(
query: string
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
const data = await get<TMDBSearchResult>(`search/multi`, {
query,
include_adult: false,
language: "en-US",
page: 1,
});
// filter out results that aren't movies or shows
const results = data.results.filter(
(r) => r.media_type === "movie" || r.media_type === "tv"
);
return results;
}
export async function generateQuickSearchMediaUrl(
query: string
): Promise<string | undefined> {
const data = await multiSearch(query);
if (data.length === 0) return undefined;
const result = data[0];
const type = result.media_type === "movie" ? "movie" : "show";
const title = result.media_type === "movie" ? result.title : result.name;
return `/media/${TMDBIdToUrlId(
TMDBMediaToMediaType(type),
result.id.toString(),
title
)}`;
}
// Conditional type which for inferring the return type based on the content type
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
? TMDBMovieData

View File

@ -46,3 +46,20 @@ export type JWSeasonMetaResult = {
season_number: number;
episodes: JWEpisodeShort[];
};
export type JWExternalIdType =
| "eidr"
| "imdb_latest"
| "imdb"
| "tmdb_latest"
| "tmdb"
| "tms";
export interface JWExternalId {
provider: JWExternalIdType;
external_id: string;
}
export interface JWDetailedMeta extends JWMediaResult {
external_ids: JWExternalId[];
}

View File

@ -306,3 +306,46 @@ export interface ExternalIdMovieSearchResult {
tv_episode_results: any[];
tv_season_results: any[];
}
export interface TMDBMovieSearchResult {
adult: boolean;
backdrop_path: string;
id: number;
title: string;
original_language: string;
original_title: string;
overview: string;
poster_path: string;
media_type: "movie";
genre_ids: number[];
popularity: number;
release_date: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBShowSearchResult {
adult: boolean;
backdrop_path: string;
id: number;
name: string;
original_language: string;
original_name: string;
overview: string;
poster_path: string;
media_type: "tv";
genre_ids: number[];
popularity: number;
first_air_date: string;
vote_average: number;
vote_count: number;
origin_country: string[];
}
export interface TMDBSearchResult {
page: number;
results: (TMDBMovieSearchResult | TMDBShowSearchResult)[];
total_pages: number;
total_results: number;
}

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TMDBMediaToId } from "@/backend/metadata/getmeta";
import { TMDBMediaToId } from "@/backend/metadata/tmdb";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { DotList } from "@/components/text/DotList";

View File

@ -5,9 +5,11 @@ import {
Switch,
useHistory,
useLocation,
useParams,
} from "react-router-dom";
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { BannerContextProvider } from "@/hooks/useBanner";
import { Layout } from "@/setup/Layout";
@ -35,6 +37,23 @@ function LegacyUrlView({ children }: { children: ReactElement }) {
return children;
}
function QuickSearch() {
const { query } = useParams<{ query: string }>();
const { replace } = useHistory();
useEffect(() => {
if (query) {
generateQuickSearchMediaUrl(query).then((url) => {
replace(url ?? "/");
});
} else {
replace("/");
}
}, [query, replace]);
return null;
}
function App() {
return (
<SettingsProvider>
@ -48,6 +67,9 @@ function App() {
<Route exact path="/">
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
</Route>
<Route exact path="/s/:query">
<QuickSearch />
</Route>
{/* pages */}
<Route exact path="/media/:media">

View File

@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { decodeTMDBId, getMetaFromId } from "@/backend/metadata/getmeta";
import { getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import {
MWMediaType,
MWSeasonWithEpisodeMeta,

View File

@ -4,11 +4,8 @@ import { useTranslation } from "react-i18next";
import { useHistory, useParams } from "react-router-dom";
import { MWStream } from "@/backend/helpers/streams";
import {
DetailedMeta,
decodeTMDBId,
getMetaFromId,
} from "@/backend/metadata/getmeta";
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import {
MWMediaType,
MWSeasonWithEpisodeMeta,

View File

@ -4764,6 +4764,11 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
slugify@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"