diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts new file mode 100644 index 00000000..c4e844d7 --- /dev/null +++ b/src/backend/metadata/search.ts @@ -0,0 +1,68 @@ +import { MWMediaType, MWQuery } from "@/providers"; + +const JW_API_BASE = "https://apis.justwatch.com"; + +type JWContentTypes = "movie" | "show"; + +type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +type JWSearchResults = { + title: string; + poster?: string; + id: number; + original_release_year: number; + jw_entity_id: string; +}; + +type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export type MWSearchResult = { + title: string; + id: string; + year: string; + poster?: string; + type: MWMediaType; +}; + +export async function searchForMedia({ + searchQuery, + type, +}: MWQuery): Promise { + const body: JWSearchQuery = { + content_types: [], + page: 1, + query: searchQuery, + page_size: 40, + }; + if (type === MWMediaType.MOVIE) body.content_types.push("movie"); + else if (type === MWMediaType.SERIES) body.content_types.push("show"); + else if (type === MWMediaType.ANIME) + throw new Error("Anime search type is not supported"); + + const data = await fetch( + `${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent( + JSON.stringify(body) + )}` + ).then((res) => res.json() as Promise>); + + return data.items.map((v) => ({ + title: v.title, + id: v.id.toString(), + year: v.original_release_year.toString(), + poster: v.poster + ? `https://images.justwatch.com${v.poster.replace("{profile}", "s166")}` + : undefined, + type, + })); +} diff --git a/src/backend/providermeta/.gitkeep b/src/backend/providermeta/.gitkeep new file mode 100644 index 00000000..37c97987 --- /dev/null +++ b/src/backend/providermeta/.gitkeep @@ -0,0 +1 @@ +this folder will be used for provider helper methods and the like diff --git a/src/backend/providers/.gitkeep b/src/backend/providers/.gitkeep new file mode 100644 index 00000000..8fbcff9c --- /dev/null +++ b/src/backend/providers/.gitkeep @@ -0,0 +1 @@ +the new list of all providers, the old ones will go and be rewritten diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 8174f2c8..34fb09b4 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,30 +1,16 @@ import { Link } from "react-router-dom"; -import { - convertMediaToPortable, - getProviderFromId, - MWMediaMeta, - MWMediaType, -} from "@/providers"; -import { serializePortableMedia } from "@/hooks/usePortableMedia"; import { DotList } from "@/components/text/DotList"; +import { MWSearchResult } from "@/backend/metadata/search"; +import { MWMediaType } from "@/providers"; export interface MediaCardProps { - media: MWMediaMeta; - // eslint-disable-next-line react/no-unused-prop-types - watchedPercentage: number; + media: MWSearchResult; linkable?: boolean; - series?: boolean; } // TODO add progress back -function MediaCardContent({ media, series, linkable }: MediaCardProps) { - const provider = getProviderFromId(media.providerId); - - if (!provider) { - return null; - } - +function MediaCardContent({ media, linkable }: MediaCardProps) { return (
-
+

{media.title} - {series && media.seasonId && media.episodeId ? ( - - S{media.seasonId} E{media.episodeId} - - ) : null}

- +
); @@ -56,17 +39,13 @@ function MediaCardContent({ media, series, linkable }: MediaCardProps) { export function MediaCard(props: MediaCardProps) { let link = "movie"; - if (props.media.mediaType === MWMediaType.SERIES) link = "series"; + if (props.media.type === MWMediaType.SERIES) link = "series"; const content = ; if (!props.linkable) return {content}; return ( - + {content} ); diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index f8338d57..f1d37374 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,23 +1,10 @@ -import { MWMediaMeta } from "@/providers"; -import { useWatchedContext, getWatchedFromPortable } from "@/state/watched"; +import { MWSearchResult } from "@/backend/metadata/search"; import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { - media: MWMediaMeta; - series?: boolean; + media: MWSearchResult; } export function WatchedMediaCard(props: WatchedMediaCardProps) { - const { watched } = useWatchedContext(); - const foundWatched = getWatchedFromPortable(watched.items, props.media); - const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; - - return ( - - ); + return ; } diff --git a/src/hooks/useLoading.ts b/src/hooks/useLoading.ts index 05411004..247a05ed 100644 --- a/src/hooks/useLoading.ts +++ b/src/hooks/useLoading.ts @@ -2,7 +2,12 @@ import React, { useMemo, useRef, useState } from "react"; export function useLoading Promise>( action: T -) { +): [ + (...args: Parameters) => ReturnType | Promise, + boolean, + Error | undefined, + boolean +] { const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(undefined); @@ -20,11 +25,11 @@ export function useLoading Promise>( const doAction = useMemo( () => - async (...args: Parameters) => { + async (...args: any) => { setLoading(true); setSuccess(false); setError(undefined); - return new Promise((resolve) => { + return new Promise((resolve) => { actionMemo(...args) .then((v) => { if (!isMounted.current) return resolve(undefined); diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 17f06642..06456b78 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -4,6 +4,15 @@ export const BookmarkStore = versionedStoreBuilder() .setKey("mw-bookmarks") .addVersion({ version: 0, + }) + .addVersion({ + version: 1, + migrate() { + return { + // TODO actually migrate + bookmarks: [], + }; + }, create() { return { bookmarks: [], diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 0b3a79f7..065de4ec 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -85,6 +85,15 @@ export const VideoProgressStore = versionedStoreBuilder() return output; }, + }) + .addVersion({ + version: 2, + migrate() { + // TODO actually migrate + return { + items: [], + }; + }, create() { return { items: [], diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5688b1c8..5314996d 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,6 +1,8 @@ +import { searchForMedia } from "@/backend/metadata/search"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +import { MWMediaType } from "@/providers"; import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 @@ -32,6 +34,14 @@ export function TestView() { return

Click me to show

; } + async function search() { + const test = await searchForMedia({ + searchQuery: "tron", + type: MWMediaType.MOVIE, + }); + console.log(test); + } + return (
@@ -44,6 +54,7 @@ export function TestView() { onProgress={(a, b) => console.log(a, b)} /> +

search()}>click me to search

); } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 6a775a23..ad611516 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -6,38 +6,26 @@ import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; -import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; +import { MWQuery } from "@/providers"; +import { MWSearchResult, searchForMedia } from "@/backend/metadata/search"; import { SearchLoadingView } from "./SearchLoadingView"; -function SearchSuffix(props: { - fails: number; - total: number; - resultsSize: number; -}) { +function SearchSuffix(props: { failed?: boolean; results?: number }) { const { t } = useTranslation(); - const allFailed: boolean = props.fails === props.total; - const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; + const icon: Icons = props.failed ? Icons.WARNING : Icons.EYE_SLASH; return (
{/* standard suffix */} - {!allFailed ? ( + {!props.failed ? (
- {props.fails > 0 ? ( -

- {t("search.providersFailed", { - fails: props.fails, - total: props.total, - })} -

- ) : null} - {props.resultsSize > 0 ? ( + {(props.results ?? 0) > 0 ? (

{t("search.allResults")}

) : (

{t("search.noResults")}

@@ -46,7 +34,7 @@ function SearchSuffix(props: { ) : null} {/* Error result */} - {allFailed ? ( + {props.failed ? (

{t("search.allFailed")}

@@ -58,9 +46,9 @@ function SearchSuffix(props: { export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { const { t } = useTranslation(); - const [results, setResults] = useState(); + const [results, setResults] = useState([]); const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => - SearchProviders(query) + searchForMedia(query) ); useEffect(() => { @@ -74,32 +62,25 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { }, [searchQuery, runSearchQuery]); if (loading) return ; - if (error) return ; + if (error) return ; if (!results) return null; return (
- {results?.results.length > 0 ? ( + {results.length > 0 ? ( - {results.results.map((v) => ( - + {results.map((v) => ( + ))} ) : null} - +
); }