mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 07:51:53 +01:00
Merge pull request #364 from castdrian/feat-urls-quicksearch
feat: human readable urls & quicksearch
This commit is contained in:
commit
2fa44b8f51
@ -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"
|
||||
},
|
||||
|
@ -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)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user