movie-web/src/state/watched/context.tsx
2023-02-11 00:43:38 +01:00

236 lines
6.2 KiB
TypeScript

import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
import {
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from "react";
import { VideoProgressStore } from "./store";
const FIVETEEN_MINUTES = 15 * 60;
const FIVE_MINUTES = 5 * 60;
function shouldSave(
time: number,
duration: number,
isSeries: boolean
): boolean {
const timeFromEnd = Math.max(0, duration - time);
// short movie
if (duration < FIVETEEN_MINUTES) {
if (time < 5) return false;
if (timeFromEnd < 60) return false;
return true;
}
// long movie
if (time < 30) return false;
if (timeFromEnd < FIVE_MINUTES && !isSeries) return false;
return true;
}
interface MediaItem {
meta: MWMediaMeta;
series?: {
episodeId: string;
seasonId: string;
episode: number;
season: number;
};
}
export interface WatchedStoreItem {
item: MediaItem;
progress: number;
percentage: number;
watchedAt: number;
}
export interface WatchedStoreData {
items: WatchedStoreItem[];
}
interface WatchedStoreDataWrapper {
updateProgress(media: MediaItem, progress: number, total: number): void;
getFilteredWatched(): WatchedStoreItem[];
removeProgress(id: string): void;
watched: WatchedStoreData;
}
const WatchedContext = createContext<WatchedStoreDataWrapper>({
updateProgress: () => {},
getFilteredWatched: () => [],
removeProgress: () => {},
watched: {
items: [],
},
});
WatchedContext.displayName = "WatchedContext";
function isSameEpisode(media: MediaItem, v: MediaItem) {
return (
media.meta.id === v.meta.id &&
(!media.series ||
(media.series.seasonId === v.series?.seasonId &&
media.series.episodeId === v.series?.episodeId))
);
}
export function WatchedContextProvider(props: { children: ReactNode }) {
const watchedLocalstorage = VideoProgressStore.get();
const [watched, setWatchedReal] = useState<WatchedStoreData>(
watchedLocalstorage as WatchedStoreData
);
const setWatched = useCallback(
(data: any) => {
setWatchedReal((old) => {
let newData = data;
if (data.constructor === Function) {
newData = data(old);
}
watchedLocalstorage.save(newData);
return newData;
});
},
[setWatchedReal, watchedLocalstorage]
);
const contextValue = useMemo(
() => ({
removeProgress(id: string) {
setWatched((data: WatchedStoreData) => {
const newData = { ...data };
newData.items = newData.items.filter((v) => v.item.meta.id !== id);
return newData;
});
},
updateProgress(media: MediaItem, progress: number, total: number): void {
setWatched((data: WatchedStoreData) => {
const newData = { ...data };
let item = newData.items.find((v) => isSameEpisode(media, v.item));
if (!item) {
item = {
item: {
...media,
meta: { ...media.meta },
series: media.series ? { ...media.series } : undefined,
},
progress: 0,
percentage: 0,
watchedAt: Date.now(),
};
newData.items.push(item);
}
// update actual item
item.progress = progress;
item.percentage = Math.round((progress / total) * 100);
item.watchedAt = Date.now();
// remove item if shouldnt save
if (!shouldSave(progress, total, !!media.series)) {
newData.items = data.items.filter(
(v) => !isSameEpisode(v.item, media)
);
}
return newData;
});
},
getFilteredWatched() {
let filtered = watched.items;
// get most recently watched for every single item
const alreadyFoundMedia: string[] = [];
filtered = filtered
.sort((a, b) => {
return b.watchedAt - a.watchedAt;
})
.filter((item) => {
const mediaId = item.item.meta.id;
if (alreadyFoundMedia.includes(mediaId)) return false;
alreadyFoundMedia.push(mediaId);
return true;
});
return filtered;
},
watched,
}),
[watched, setWatched]
);
return (
<WatchedContext.Provider value={contextValue as any}>
{props.children}
</WatchedContext.Provider>
);
}
export function useWatchedContext() {
return useContext(WatchedContext);
}
function isSameEpisodeMeta(
media: MediaItem,
mediaTwo: DetailedMeta | null,
episodeId?: string
) {
if (mediaTwo?.meta.type === MWMediaType.SERIES && episodeId) {
return isSameEpisode(media, {
meta: mediaTwo.meta,
series: {
season: 0,
episode: 0,
episodeId,
seasonId: mediaTwo.meta.seasonData.id,
},
});
}
if (!mediaTwo) return () => false;
return isSameEpisode(media, { meta: mediaTwo.meta });
}
export function useWatchedItem(meta: DetailedMeta | null, episodeId?: string) {
const { watched, updateProgress } = useContext(WatchedContext);
const item = useMemo(
() => watched.items.find((v) => isSameEpisodeMeta(v.item, meta, episodeId)),
[watched, meta, episodeId]
);
const lastCommitedTime = useRef([0, 0]);
const callback = useCallback(
(progress: number, total: number) => {
const hasChanged =
lastCommitedTime.current[0] !== progress ||
lastCommitedTime.current[1] !== total;
if (meta && hasChanged) {
lastCommitedTime.current = [progress, total];
const obj = {
meta: meta.meta,
series:
meta.meta.type === MWMediaType.SERIES && episodeId
? {
seasonId: meta.meta.seasonData.id,
episodeId,
season: meta.meta.seasonData.number,
episode:
meta.meta.seasonData.episodes.find(
(ep) => ep.id === episodeId
)?.number || 0,
}
: undefined,
};
updateProgress(obj, progress, total);
}
},
[meta, updateProgress, episodeId]
);
return { updateProgress: callback, watchedItem: item };
}