1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

searching of subs + caching of results + sort subs by common usage + better loading state for subs + PiP added to mobile + remove useLoading

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-22 22:07:45 +02:00
parent 9ce0e6a099
commit 8e65db04a3
9 changed files with 128 additions and 101 deletions

View file

@ -2,7 +2,7 @@ import Fuse from "fuse.js";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { useAsync, useAsyncFn } from "react-use"; import { useAsync, useAsyncFn } from "react-use";
import { languageIdToName } from "@/backend/helpers/subs"; import { SubtitleSearchItem, languageIdToName } from "@/backend/helpers/subs";
import { FlagIcon } from "@/components/FlagIcon"; import { FlagIcon } from "@/components/FlagIcon";
import { useCaptions } from "@/components/player/hooks/useCaptions"; import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
@ -15,7 +15,9 @@ export function CaptionOption(props: {
countryCode?: string; countryCode?: string;
children: React.ReactNode; children: React.ReactNode;
selected?: boolean; selected?: boolean;
loading?: boolean;
onClick?: () => void; onClick?: () => void;
error?: React.ReactNode;
}) { }) {
// Country code overrides // Country code overrides
const countryOverrides: Record<string, string> = { const countryOverrides: Record<string, string> = {
@ -34,7 +36,12 @@ export function CaptionOption(props: {
countryCode = countryOverrides[countryCode]; countryCode = countryOverrides[countryCode];
return ( return (
<SelectableLink selected={props.selected} onClick={props.onClick}> <SelectableLink
selected={props.selected}
loading={props.loading}
error={props.error}
onClick={props.onClick}
>
<span className="flex items-center"> <span className="flex items-center">
<span data-code={props.countryCode} className="mr-3"> <span data-code={props.countryCode} className="mr-3">
<FlagIcon countryCode={countryCode} /> <FlagIcon countryCode={countryCode} />
@ -45,12 +52,46 @@ export function CaptionOption(props: {
); );
} }
// TODO cache like everything in this view function searchSubs(
subs: (SubtitleSearchItem & { languageName: string })[],
searchQuery: string
) {
const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why
let results = subs.sort((a, b) => {
if (
languagesOrder.indexOf(b.attributes.language) !== -1 ||
languagesOrder.indexOf(a.attributes.language) !== -1
)
return (
languagesOrder.indexOf(b.attributes.language) -
languagesOrder.indexOf(a.attributes.language)
);
return a.languageName.localeCompare(b.languageName);
});
if (searchQuery.trim().length > 0) {
const fuse = new Fuse(subs, {
includeScore: true,
keys: ["languageName"],
});
results = fuse.search(searchQuery).map((res) => res.item);
}
return results;
}
// TODO on initialize, download captions
// TODO fix language names, some are unknown // TODO fix language names, some are unknown
// TODO sort languages by common usage // TODO delay setting for captions
export function CaptionsView({ id }: { id: string }) { export function CaptionsView({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const lang = usePlayerStore((s) => s.caption.selected?.language); const lang = usePlayerStore((s) => s.caption.selected?.language);
const [currentlyDownloading, setCurrentlyDownloading] = useState<
string | null
>(null);
const { search, download, disable } = useCaptions(); const { search, download, disable } = useCaptions();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -58,8 +99,11 @@ export function CaptionsView({ id }: { id: string }) {
const req = useAsync(async () => search(), [search]); const req = useAsync(async () => search(), [search]);
const [downloadReq, startDownload] = useAsyncFn( const [downloadReq, startDownload] = useAsyncFn(
(subtitleId: string, language: string) => download(subtitleId, language), async (subtitleId: string, language: string) => {
[download] setCurrentlyDownloading(subtitleId);
return download(subtitleId, language);
},
[download, setCurrentlyDownloading]
); );
let downloadProgress: ReactNode = null; let downloadProgress: ReactNode = null;
@ -78,22 +122,22 @@ export function CaptionsView({ id }: { id: string }) {
}; };
}); });
let results = subs; content = searchSubs(subs, searchQuery).map((v) => {
if (searchQuery.trim().length > 0) {
const fuse = new Fuse(subs, {
includeScore: true,
keys: ["languageName"],
});
results = fuse.search(searchQuery).map((res) => res.item);
}
content = results.map((v) => {
return ( return (
<CaptionOption <CaptionOption
key={v.id} key={v.id}
countryCode={v.attributes.language} countryCode={v.attributes.language}
selected={lang === v.attributes.language} selected={lang === v.attributes.language}
loading={
v.attributes.legacy_subtitle_id === currentlyDownloading &&
downloadReq.loading
}
error={
v.attributes.legacy_subtitle_id === currentlyDownloading &&
downloadReq.error
? downloadReq.error
: undefined
}
onClick={() => onClick={() =>
startDownload( startDownload(
v.attributes.legacy_subtitle_id, v.attributes.legacy_subtitle_id,

View file

@ -80,18 +80,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
(v) => v.height === qualityToHlsLevel(availableQuality) (v) => v.height === qualityToHlsLevel(availableQuality)
); );
if (levelIndex !== -1) { if (levelIndex !== -1) {
console.log("setting level", levelIndex, availableQuality);
hls.currentLevel = levelIndex; hls.currentLevel = levelIndex;
hls.loadLevel = levelIndex; hls.loadLevel = levelIndex;
} }
} }
} else { } else {
console.log("setting to automatic");
hls.currentLevel = -1; hls.currentLevel = -1;
hls.loadLevel = -1; hls.loadLevel = -1;
} }
const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]);
console.log("updating quality menu", quality);
emit("changedquality", quality); emit("changedquality", quality);
} }
@ -117,7 +114,6 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
hls.on(Hls.Events.LEVEL_SWITCHED, () => { hls.on(Hls.Events.LEVEL_SWITCHED, () => {
if (!hls) return; if (!hls) return;
const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]);
console.log("EVENT updating quality menu", quality);
emit("changedquality", quality); emit("changedquality", quality);
}); });
} }

View file

@ -1,8 +1,26 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { downloadSrt, searchSubtitles } from "@/backend/helpers/subs"; import {
SubtitleSearchItem,
downloadSrt,
searchSubtitles,
} from "@/backend/helpers/subs";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles"; import { useSubtitleStore } from "@/stores/subtitles";
import { SimpleCache } from "@/utils/cache";
const cacheTimeSec = 24 * 60 * 60; // 24 hours
const downloadCache = new SimpleCache<string, string>();
downloadCache.setCompare((a, b) => a === b);
const searchCache = new SimpleCache<
{ tmdbId: string; ep?: string; season?: string },
SubtitleSearchItem[]
>();
searchCache.setCompare(
(a, b) => a.tmdbId === b.tmdbId && a.ep === b.ep && a.season === b.season
);
export function useCaptions() { export function useCaptions() {
const setLanguage = useSubtitleStore((s) => s.setLanguage); const setLanguage = useSubtitleStore((s) => s.setLanguage);
@ -13,7 +31,11 @@ export function useCaptions() {
const download = useCallback( const download = useCallback(
async (subtitleId: string, language: string) => { async (subtitleId: string, language: string) => {
const srtData = await downloadSrt(subtitleId); let srtData = downloadCache.get(subtitleId);
if (!srtData) {
srtData = await downloadSrt(subtitleId);
downloadCache.set(subtitleId, srtData, cacheTimeSec);
}
setCaption({ setCaption({
language, language,
srtData, srtData,
@ -26,7 +48,17 @@ export function useCaptions() {
const search = useCallback(async () => { const search = useCallback(async () => {
if (!meta) throw new Error("No meta"); if (!meta) throw new Error("No meta");
return searchSubtitles(meta); const key = {
tmdbId: meta.tmdbId,
ep: meta.episode?.tmdbId,
season: meta.season?.tmdbId,
};
const results = searchCache.get(key);
if (results) return [...results];
const freshResults = await searchSubtitles(meta);
searchCache.set(key, [...freshResults], cacheTimeSec);
return freshResults;
}, [meta]); }, [meta]);
const disable = useCallback(async () => { const disable = useCallback(async () => {

View file

@ -2,6 +2,7 @@ import classNames from "classnames";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { Spinner } from "@/components/layout/Spinner";
import { Title } from "@/components/player/internals/ContextMenu/Misc"; import { Title } from "@/components/player/internals/ContextMenu/Misc";
export function Chevron(props: { children?: React.ReactNode }) { export function Chevron(props: { children?: React.ReactNode }) {
@ -112,21 +113,34 @@ export function ChevronLink(props: {
export function SelectableLink(props: { export function SelectableLink(props: {
selected?: boolean; selected?: boolean;
loading?: boolean;
onClick?: () => void; onClick?: () => void;
children?: ReactNode; children?: ReactNode;
disabled?: boolean; disabled?: boolean;
error?: ReactNode;
}) { }) {
const rightContent = ( let rightContent;
<Icon if (props.selected) {
icon={Icons.CIRCLE_CHECK} rightContent = (
className="text-xl text-video-context-type-accent" <Icon
/> icon={Icons.CIRCLE_CHECK}
); className="text-xl text-video-context-type-accent"
/>
);
}
if (props.error)
rightContent = (
<span className="flex items-center text-video-context-error">
<Icon className="ml-2" icon={Icons.WARNING} />
</span>
);
if (props.loading) rightContent = <Spinner className="text-xl" />; // should override selected and error
return ( return (
<Link <Link
onClick={props.onClick} onClick={props.onClick}
clickable={!props.disabled} clickable={!props.disabled}
rightSide={props.selected ? rightContent : null} rightSide={rightContent}
> >
<LinkTitle <LinkTitle
textClass={classNames({ textClass={classNames({

View file

@ -34,17 +34,11 @@ export function useChromecast() {
request.autoplay = true; request.autoplay = true;
const session = instance.current?.getCurrentSession(); const session = instance.current?.getCurrentSession();
console.log("testing", session);
if (!session) return; if (!session) return;
session session.loadMedia(request).catch((e: any) => {
.loadMedia(request) console.error(e);
.then(() => { });
console.log("Media is loaded");
})
.catch((e: any) => {
console.error(e);
});
} }
function stopCast() { function stopCast() {

View file

@ -1,53 +0,0 @@
import React, { useMemo, useRef, useState } from "react";
export function useLoading<T extends (...args: any) => Promise<any>>(
action: T
): [
(...args: Parameters<T>) => ReturnType<T> | Promise<undefined>,
boolean,
Error | undefined,
boolean
] {
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<any | undefined>(undefined);
const isMounted = useRef(true);
// we want action to be memoized forever
const actionMemo = useMemo(() => action, []); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const doAction = useMemo(
() =>
async (...args: any) => {
setLoading(true);
setSuccess(false);
setError(undefined);
return new Promise<any>((resolve) => {
actionMemo(...args)
.then((v) => {
if (!isMounted.current) return resolve(undefined);
setSuccess(true);
resolve(v);
return null;
})
.catch((err) => {
if (isMounted) {
setError(err);
console.error("USELOADING ERROR", err);
setSuccess(false);
}
resolve(undefined);
});
}).finally(() => isMounted.current && setLoading(false));
},
[actionMemo]
);
return [doAction, loading, error, success];
}

View file

@ -89,6 +89,7 @@ export function PlayerPart(props: PlayerPartProps) {
<div className="grid grid-cols-[2.5rem,1fr,2.5rem] gap-3 lg:hidden"> <div className="grid grid-cols-[2.5rem,1fr,2.5rem] gap-3 lg:hidden">
<div /> <div />
<div className="flex justify-center space-x-3"> <div className="flex justify-center space-x-3">
<Player.Pip />
<Player.Episodes /> <Player.Episodes />
<Player.Settings /> <Player.Settings />
</div> </div>

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use";
import { searchForMedia } from "@/backend/metadata/search"; import { searchForMedia } from "@/backend/metadata/search";
import { MWQuery } from "@/backend/metadata/types/mw"; import { MWQuery } from "@/backend/metadata/types/mw";
@ -8,7 +9,6 @@ import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid"; import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useLoading } from "@/hooks/useLoading";
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart"; import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
import { MediaItem } from "@/utils/mediaTypes"; import { MediaItem } from "@/utils/mediaTypes";
@ -49,22 +49,20 @@ export function SearchListPart({ searchQuery }: { searchQuery: string }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [results, setResults] = useState<MediaItem[]>([]); const [results, setResults] = useState<MediaItem[]>([]);
const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => const [state, exec] = useAsyncFn((query: MWQuery) => searchForMedia(query));
searchForMedia(query)
);
useEffect(() => { useEffect(() => {
async function runSearch(query: MWQuery) { async function runSearch(query: MWQuery) {
const searchResults = await runSearchQuery(query); const searchResults = await exec(query);
if (!searchResults) return; if (!searchResults) return;
setResults(searchResults); setResults(searchResults);
} }
if (searchQuery !== "") runSearch({ searchQuery }); if (searchQuery !== "") runSearch({ searchQuery });
}, [searchQuery, runSearchQuery]); }, [searchQuery, exec]);
if (loading) return <SearchLoadingPart />; if (state.loading) return <SearchLoadingPart />;
if (error) return <SearchSuffix failed />; if (state.error) return <SearchSuffix failed />;
if (!results) return null; if (!results) return null;
return ( return (

View file

@ -152,6 +152,7 @@ module.exports = {
cardBorder: "#1B262E", cardBorder: "#1B262E",
slider: "#8787A8", slider: "#8787A8",
sliderFilled: "#A75FC9", sliderFilled: "#A75FC9",
error: "#E44F4F",
download: { download: {
button: "#6b298a", button: "#6b298a",