mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Merge pull request #364 from castdrian/feat-urls-quicksearch
feat: human readable urls & quicksearch
This commit is contained in:
commit
2fa44b8f51
10 changed files with 152 additions and 51 deletions
|
@ -32,6 +32,7 @@
|
||||||
"react-stickynode": "^4.1.0",
|
"react-stickynode": "^4.1.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"subsrt-ts": "^2.1.1",
|
"subsrt-ts": "^2.1.1",
|
||||||
"unpacker": "^1.0.1"
|
"unpacker": "^1.0.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { FetchError } from "ofetch";
|
||||||
|
|
||||||
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
|
import { formatJWMeta, mediaTypeToJW } from "./justwatch";
|
||||||
import {
|
import {
|
||||||
|
TMDBIdToUrlId,
|
||||||
TMDBMediaToMediaType,
|
TMDBMediaToMediaType,
|
||||||
formatTMDBMeta,
|
formatTMDBMeta,
|
||||||
getEpisodes,
|
getEpisodes,
|
||||||
|
@ -12,7 +13,7 @@ import {
|
||||||
mediaTypeToTMDB,
|
mediaTypeToTMDB,
|
||||||
} from "./tmdb";
|
} from "./tmdb";
|
||||||
import {
|
import {
|
||||||
JWMediaResult,
|
JWDetailedMeta,
|
||||||
JWSeasonMetaResult,
|
JWSeasonMetaResult,
|
||||||
JW_API_BASE,
|
JW_API_BASE,
|
||||||
} from "./types/justwatch";
|
} from "./types/justwatch";
|
||||||
|
@ -25,23 +26,6 @@ import {
|
||||||
} from "./types/tmdb";
|
} from "./types/tmdb";
|
||||||
import { makeUrl, proxiedFetch } from "../helpers/fetch";
|
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 {
|
export interface DetailedMeta {
|
||||||
meta: MWMediaMeta;
|
meta: MWMediaMeta;
|
||||||
imdbId?: string;
|
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 {
|
export function isLegacyUrl(url: string): boolean {
|
||||||
if (url.startsWith("/media/JW")) return true;
|
if (url.startsWith("/media/JW")) return true;
|
||||||
return false;
|
return false;
|
||||||
|
@ -224,10 +187,12 @@ export async function convertLegacyUrl(
|
||||||
// movies always have an imdb id on tmdb
|
// movies always have an imdb id on tmdb
|
||||||
if (imdbId && mediaType === MWMediaType.MOVIE) {
|
if (imdbId && mediaType === MWMediaType.MOVIE) {
|
||||||
const movieId = await getMovieFromExternalId(imdbId);
|
const movieId = await getMovieFromExternalId(imdbId);
|
||||||
if (movieId) return `/media/tmdb-movie-${movieId}`;
|
if (movieId) {
|
||||||
|
return `/media/${TMDBIdToUrlId(mediaType, movieId, meta.meta.title)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
return `/media/tmdb-${type}-${tmdbId}`;
|
return `/media/${TMDBIdToUrlId(mediaType, tmdbId, meta.meta.title)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import slugify from "slugify";
|
||||||
|
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
|
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw";
|
||||||
|
@ -11,12 +13,15 @@ import {
|
||||||
TMDBMovieExternalIds,
|
TMDBMovieExternalIds,
|
||||||
TMDBMovieResponse,
|
TMDBMovieResponse,
|
||||||
TMDBMovieResult,
|
TMDBMovieResult,
|
||||||
|
TMDBMovieSearchResult,
|
||||||
|
TMDBSearchResult,
|
||||||
TMDBSeason,
|
TMDBSeason,
|
||||||
TMDBSeasonMetaResult,
|
TMDBSeasonMetaResult,
|
||||||
TMDBShowData,
|
TMDBShowData,
|
||||||
TMDBShowExternalIds,
|
TMDBShowExternalIds,
|
||||||
TMDBShowResponse,
|
TMDBShowResponse,
|
||||||
TMDBShowResult,
|
TMDBShowResult,
|
||||||
|
TMDBShowSearchResult,
|
||||||
} from "./types/tmdb";
|
} from "./types/tmdb";
|
||||||
import { mwFetch } from "../helpers/fetch";
|
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 {
|
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(
|
export function decodeTMDBId(
|
||||||
|
@ -143,6 +161,38 @@ export async function searchMedia(
|
||||||
return data;
|
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
|
// Conditional type which for inferring the return type based on the content type
|
||||||
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
|
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
|
||||||
? TMDBMovieData
|
? TMDBMovieData
|
||||||
|
|
|
@ -46,3 +46,20 @@ export type JWSeasonMetaResult = {
|
||||||
season_number: number;
|
season_number: number;
|
||||||
episodes: JWEpisodeShort[];
|
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_episode_results: any[];
|
||||||
tv_season_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 { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
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 { MWMediaMeta } from "@/backend/metadata/types/mw";
|
||||||
import { DotList } from "@/components/text/DotList";
|
import { DotList } from "@/components/text/DotList";
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,11 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
useHistory,
|
useHistory,
|
||||||
useLocation,
|
useLocation,
|
||||||
|
useParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
|
||||||
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
||||||
|
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
||||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { BannerContextProvider } from "@/hooks/useBanner";
|
import { BannerContextProvider } from "@/hooks/useBanner";
|
||||||
import { Layout } from "@/setup/Layout";
|
import { Layout } from "@/setup/Layout";
|
||||||
|
@ -35,6 +37,23 @@ function LegacyUrlView({ children }: { children: ReactElement }) {
|
||||||
return children;
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
|
@ -48,6 +67,9 @@ function App() {
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/s/:query">
|
||||||
|
<QuickSearch />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* pages */}
|
{/* pages */}
|
||||||
<Route exact path="/media/:media">
|
<Route exact path="/media/:media">
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useParams } from "react-router-dom";
|
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 {
|
import {
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
MWSeasonWithEpisodeMeta,
|
MWSeasonWithEpisodeMeta,
|
||||||
|
|
|
@ -4,11 +4,8 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import {
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
DetailedMeta,
|
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
||||||
decodeTMDBId,
|
|
||||||
getMetaFromId,
|
|
||||||
} from "@/backend/metadata/getmeta";
|
|
||||||
import {
|
import {
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
MWSeasonWithEpisodeMeta,
|
MWSeasonWithEpisodeMeta,
|
||||||
|
|
|
@ -4764,6 +4764,11 @@ slice-ansi@^5.0.0:
|
||||||
ansi-styles "^6.0.0"
|
ansi-styles "^6.0.0"
|
||||||
is-fullwidth-code-point "^4.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:
|
source-map-js@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
|
Loading…
Reference in a new issue