mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-29 16:06:07 +00:00
error handling for video + bookmark migration + last watched episode shown + progress migrations
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
4c43208deb
commit
023a850e4f
|
@ -14,8 +14,9 @@ export interface WatchedMediaCardProps {
|
||||||
function formatSeries(obj: ProgressMediaItem | undefined) {
|
function formatSeries(obj: ProgressMediaItem | undefined) {
|
||||||
if (!obj) return undefined;
|
if (!obj) return undefined;
|
||||||
if (obj.type !== "show") return;
|
if (obj.type !== "show") return;
|
||||||
// TODO only show latest episode watched
|
const ep = Object.values(obj.episodes).sort(
|
||||||
const ep = Object.values(obj.episodes)[0];
|
(a, b) => b.updatedAt - a.updatedAt
|
||||||
|
)[0];
|
||||||
const season = obj.seasons[ep?.seasonId];
|
const season = obj.seasons[ep?.seasonId];
|
||||||
if (!ep || !season) return;
|
if (!ep || !season) return;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
DisplayInterfaceEvents,
|
DisplayInterfaceEvents,
|
||||||
} from "@/components/player/display/displayInterface";
|
} from "@/components/player/display/displayInterface";
|
||||||
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
||||||
|
import { getMediaErrorDetails } from "@/components/player/utils/mediaErrorDetails";
|
||||||
import {
|
import {
|
||||||
LoadableSource,
|
LoadableSource,
|
||||||
SourceQuality,
|
SourceQuality,
|
||||||
|
@ -119,9 +120,12 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
console.error("HLS error", data);
|
console.error("HLS error", data);
|
||||||
if (data.fatal) {
|
if (data.fatal) {
|
||||||
throw new Error(
|
emit("error", {
|
||||||
`HLS ERROR:${data.error?.message ?? "Something went wrong"}`
|
message: data.error.message,
|
||||||
);
|
stackTrace: data.error.stack,
|
||||||
|
errorName: data.error.name,
|
||||||
|
type: "hls",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hls.on(Hls.Events.MANIFEST_LOADED, () => {
|
hls.on(Hls.Events.MANIFEST_LOADED, () => {
|
||||||
|
@ -154,6 +158,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
emit("play", undefined);
|
emit("play", undefined);
|
||||||
emit("loading", false);
|
emit("loading", false);
|
||||||
});
|
});
|
||||||
|
videoElement.addEventListener("error", () => {
|
||||||
|
const err = videoElement?.error ?? null;
|
||||||
|
const errorDetails = getMediaErrorDetails(err);
|
||||||
|
emit("error", {
|
||||||
|
errorName: errorDetails.name,
|
||||||
|
message: errorDetails.message,
|
||||||
|
type: "htmlvideo",
|
||||||
|
});
|
||||||
|
});
|
||||||
videoElement.addEventListener("playing", () => emit("play", undefined));
|
videoElement.addEventListener("playing", () => emit("play", undefined));
|
||||||
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
||||||
videoElement.addEventListener("canplay", () => emit("loading", false));
|
videoElement.addEventListener("canplay", () => emit("loading", false));
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
||||||
import { Listener } from "@/utils/events";
|
import { Listener } from "@/utils/events";
|
||||||
|
|
||||||
|
export type DisplayErrorType = "hls" | "htmlvideo";
|
||||||
|
export type DisplayError = {
|
||||||
|
stackTrace?: string;
|
||||||
|
message: string;
|
||||||
|
errorName: string;
|
||||||
|
type: DisplayErrorType;
|
||||||
|
};
|
||||||
|
|
||||||
export type DisplayInterfaceEvents = {
|
export type DisplayInterfaceEvents = {
|
||||||
play: void;
|
play: void;
|
||||||
pause: void;
|
pause: void;
|
||||||
|
@ -15,6 +23,7 @@ export type DisplayInterfaceEvents = {
|
||||||
needstrack: boolean;
|
needstrack: boolean;
|
||||||
canairplay: boolean;
|
canairplay: boolean;
|
||||||
playbackrate: number;
|
playbackrate: number;
|
||||||
|
error: DisplayError;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface qualityChangeOptions {
|
export interface qualityChangeOptions {
|
||||||
|
|
36
src/components/player/utils/mediaErrorDetails.ts
Normal file
36
src/components/player/utils/mediaErrorDetails.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const mediaErrorMap: Record<number, { name: string; message: string }> = {
|
||||||
|
1: {
|
||||||
|
name: "MEDIA_ERR_ABORTED",
|
||||||
|
message:
|
||||||
|
"The fetching of the associated resource was aborted by the user's request.",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
name: "MEDIA_ERR_NETWORK",
|
||||||
|
message:
|
||||||
|
"Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
name: "MEDIA_ERR_DECODE",
|
||||||
|
message:
|
||||||
|
"Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
name: "MEDIA_ERR_SRC_NOT_SUPPORTED",
|
||||||
|
message:
|
||||||
|
"The associated resource or media provider object has been found to be unsuitable.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMediaErrorDetails(err: MediaError | null): {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
} {
|
||||||
|
const item = mediaErrorMap[err?.code ?? -1];
|
||||||
|
if (!item) {
|
||||||
|
return {
|
||||||
|
name: "MediaError",
|
||||||
|
message: "Unknown media error occured",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { convertRunoutputToSource } from "@/components/player/utils/convertRunou
|
||||||
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
||||||
import { useQueryParam } from "@/hooks/useQueryParams";
|
import { useQueryParam } from "@/hooks/useQueryParams";
|
||||||
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
||||||
|
import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart";
|
||||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||||
import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
|
import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
|
||||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||||
|
@ -108,6 +109,7 @@ export function PlayerView() {
|
||||||
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
||||||
<ScrapeErrorPart data={errorData} />
|
<ScrapeErrorPart data={errorData} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null}
|
||||||
</PlayerPart>
|
</PlayerPart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Dropdown } from "@/components/Dropdown";
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
||||||
|
import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart";
|
||||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
import { SourceSliceSource, StreamType } from "@/stores/player/utils/qualities";
|
import { SourceSliceSource, StreamType } from "@/stores/player/utils/qualities";
|
||||||
|
@ -105,6 +106,7 @@ export default function VideoTesterView() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null}
|
||||||
</PlayerPart>
|
</PlayerPart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
56
src/pages/parts/player/PlaybackErrorPart.tsx
Normal file
56
src/pages/parts/player/PlaybackErrorPart.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { IconPill } from "@/components/layout/IconPill";
|
||||||
|
import { Paragraph } from "@/components/text/Paragraph";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
export function PlaybackErrorPart() {
|
||||||
|
const playbackError = usePlayerStore((s) => s.interface.error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
|
<ErrorContainer>
|
||||||
|
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||||
|
<Title>Goo goo gaa gaa</Title>
|
||||||
|
<Paragraph>
|
||||||
|
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
||||||
|
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
||||||
|
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
||||||
|
you find it in your heart to forgive? UwU 💖
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
href="/"
|
||||||
|
theme="purple"
|
||||||
|
padding="md:px-12 p-2.5"
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
Go home
|
||||||
|
</Button>
|
||||||
|
</ErrorContainer>
|
||||||
|
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||||
|
{/* Error */}
|
||||||
|
{playbackError ? (
|
||||||
|
<div className="w-full bg-errors-card p-6 rounded-lg">
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-errors-border">
|
||||||
|
<span className="text-white font-medium">Error details</span>
|
||||||
|
<div className="flex justify-center items-center gap-3">
|
||||||
|
<Button theme="secondary" padding="p-2 md:px-4">
|
||||||
|
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button theme="secondary" padding="p-2 md:px-2">
|
||||||
|
<Icon icon={Icons.X} className="text-2xl" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 h-60 overflow-y-auto text-left whitespace-pre pointer-events-auto">
|
||||||
|
{playbackError.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ErrorContainer>
|
||||||
|
</ErrorLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
|
||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { BookmarkStoreData } from "./types";
|
import { BookmarkStoreData } from "./types";
|
||||||
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
||||||
import { migrateV2Bookmarks } from "../watched/migrations/v3";
|
import { migrateV2Bookmarks } from "../watched/migrations/v3";
|
||||||
|
|
||||||
|
const typeMap: Record<MWMediaType, "show" | "movie" | null> = {
|
||||||
|
[MWMediaType.ANIME]: null,
|
||||||
|
[MWMediaType.MOVIE]: "movie",
|
||||||
|
[MWMediaType.SERIES]: "show",
|
||||||
|
};
|
||||||
|
|
||||||
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
||||||
.setKey("mw-bookmarks")
|
.setKey("mw-bookmarks")
|
||||||
.addVersion({
|
.addVersion({
|
||||||
|
@ -20,6 +28,28 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
||||||
})
|
})
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 2,
|
version: 2,
|
||||||
|
migrate(old: BookmarkStoreData): BookmarkStoreData {
|
||||||
|
const newItems: Record<string, BookmarkMediaItem> = {};
|
||||||
|
|
||||||
|
for (const oldBookmark of old.bookmarks) {
|
||||||
|
const type = typeMap[oldBookmark.type];
|
||||||
|
if (!type) continue;
|
||||||
|
newItems[oldBookmark.id] = {
|
||||||
|
title: oldBookmark.title,
|
||||||
|
year: oldBookmark.year ? Number(oldBookmark.year) : undefined,
|
||||||
|
poster: oldBookmark.poster,
|
||||||
|
type,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
useBookmarkStore.getState().replaceBookmarks(newItems);
|
||||||
|
|
||||||
|
return { bookmarks: [] };
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addVersion({
|
||||||
|
version: 3,
|
||||||
create() {
|
create() {
|
||||||
return {
|
return {
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
|
||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { OldData, migrateV2Videos } from "./migrations/v2";
|
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||||
|
@ -28,6 +30,93 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||||
})
|
})
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 3,
|
version: 3,
|
||||||
|
migrate(old: WatchedStoreData): WatchedStoreData {
|
||||||
|
console.log(old);
|
||||||
|
|
||||||
|
// Convert items
|
||||||
|
const newItems: Record<string, ProgressMediaItem> = {};
|
||||||
|
|
||||||
|
for (const oldItem of old.items) {
|
||||||
|
if (oldItem.item.meta.type === MWMediaType.SERIES) {
|
||||||
|
// Upsert
|
||||||
|
if (!newItems[oldItem.item.meta.id]) {
|
||||||
|
newItems[oldItem.item.meta.id] = {
|
||||||
|
type: "show",
|
||||||
|
episodes: {},
|
||||||
|
seasons: {},
|
||||||
|
title: oldItem.item.meta.title,
|
||||||
|
updatedAt: oldItem.watchedAt,
|
||||||
|
poster: oldItem.item.meta.poster,
|
||||||
|
year: Number(oldItem.item.meta.year),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add episodes
|
||||||
|
if (
|
||||||
|
oldItem.item.series &&
|
||||||
|
!newItems[oldItem.item.meta.id].episodes[
|
||||||
|
oldItem.item.series.episodeId
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
// Find episode ID (barely ever works)
|
||||||
|
const episodeTitle = oldItem.item.meta.seasonData.episodes.find(
|
||||||
|
(ep) => ep.id === oldItem.item.series?.episodeId
|
||||||
|
)?.title;
|
||||||
|
|
||||||
|
// Add season to season data
|
||||||
|
newItems[oldItem.item.meta.id].seasons[
|
||||||
|
oldItem.item.series.seasonId
|
||||||
|
] = {
|
||||||
|
id: oldItem.item.series.seasonId,
|
||||||
|
number: oldItem.item.series.season,
|
||||||
|
title:
|
||||||
|
oldItem.item.meta.seasons.find(
|
||||||
|
(s) => s.number === oldItem.item.series?.season
|
||||||
|
)?.title || "Unknown season",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate episode data
|
||||||
|
newItems[oldItem.item.meta.id].episodes[
|
||||||
|
oldItem.item.series.episodeId
|
||||||
|
] = {
|
||||||
|
title: episodeTitle || "Unknown",
|
||||||
|
id: oldItem.item.series.episodeId,
|
||||||
|
number: oldItem.item.series.episode,
|
||||||
|
seasonId: oldItem.item.series.seasonId,
|
||||||
|
updatedAt: oldItem.watchedAt,
|
||||||
|
progress: {
|
||||||
|
duration: (100 / oldItem.percentage) * oldItem.progress,
|
||||||
|
watched: oldItem.progress,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newItems[oldItem.item.meta.id] = {
|
||||||
|
type: "movie",
|
||||||
|
episodes: {},
|
||||||
|
seasons: {},
|
||||||
|
title: oldItem.item.meta.title,
|
||||||
|
updatedAt: oldItem.watchedAt,
|
||||||
|
year: Number(oldItem.item.meta.year),
|
||||||
|
poster: oldItem.item.meta.poster,
|
||||||
|
progress: {
|
||||||
|
duration: (100 / oldItem.percentage) * oldItem.progress,
|
||||||
|
watched: oldItem.progress,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(newItems);
|
||||||
|
useProgressStore.getState().replaceItems(newItems);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addVersion({
|
||||||
|
version: 4,
|
||||||
create() {
|
create() {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
export interface BookmarkMediaItem {
|
export interface BookmarkMediaItem {
|
||||||
title: string;
|
title: string;
|
||||||
year: number;
|
year?: number;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
type: "show" | "movie";
|
type: "show" | "movie";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
@ -16,9 +16,9 @@ export interface ProgressStore {
|
||||||
bookmarks: Record<string, BookmarkMediaItem>;
|
bookmarks: Record<string, BookmarkMediaItem>;
|
||||||
addBookmark(meta: PlayerMeta): void;
|
addBookmark(meta: PlayerMeta): void;
|
||||||
removeBookmark(id: string): void;
|
removeBookmark(id: string): void;
|
||||||
|
replaceBookmarks(items: Record<string, BookmarkMediaItem>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add migration from previous bookmark store
|
|
||||||
export const useBookmarkStore = create(
|
export const useBookmarkStore = create(
|
||||||
persist(
|
persist(
|
||||||
immer<ProgressStore>((set) => ({
|
immer<ProgressStore>((set) => ({
|
||||||
|
@ -39,6 +39,11 @@ export const useBookmarkStore = create(
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
replaceBookmarks(items: Record<string, BookmarkMediaItem>) {
|
||||||
|
set((s) => {
|
||||||
|
s.bookmarks = items;
|
||||||
|
});
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "__MW::bookmarks",
|
name: "__MW::bookmarks",
|
||||||
|
|
|
@ -90,6 +90,12 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
|
||||||
s.mediaPlaying.playbackRate = rate;
|
s.mediaPlaying.playbackRate = rate;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
newDisplay.on("error", (err) => {
|
||||||
|
set((s) => {
|
||||||
|
s.status = playerStatus.PLAYBACK_ERROR;
|
||||||
|
s.interface.error = err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.display = newDisplay;
|
s.display = newDisplay;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DisplayError } from "@/components/player/display/displayInterface";
|
||||||
import { MakeSlice } from "@/stores/player/slices/types";
|
import { MakeSlice } from "@/stores/player/slices/types";
|
||||||
|
|
||||||
export enum VideoPlayerTimeFormat {
|
export enum VideoPlayerTimeFormat {
|
||||||
|
@ -23,6 +24,7 @@ export interface InterfaceSlice {
|
||||||
isCasting: boolean;
|
isCasting: boolean;
|
||||||
hideNextEpisodeBtn: boolean;
|
hideNextEpisodeBtn: boolean;
|
||||||
shouldStartFromBeginning: boolean;
|
shouldStartFromBeginning: boolean;
|
||||||
|
error?: DisplayError;
|
||||||
|
|
||||||
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
||||||
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
|
volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig"
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const playerStatus = {
|
||||||
SCRAPING: "scraping",
|
SCRAPING: "scraping",
|
||||||
PLAYING: "playing",
|
PLAYING: "playing",
|
||||||
SCRAPE_NOT_FOUND: "scrapeNotFound",
|
SCRAPE_NOT_FOUND: "scrapeNotFound",
|
||||||
|
PLAYBACK_ERROR: "playbackError",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PlayerStatus = ValuesOf<typeof playerStatus>;
|
export type PlayerStatus = ValuesOf<typeof playerStatus>;
|
||||||
|
|
|
@ -20,12 +20,13 @@ export interface ProgressEpisodeItem {
|
||||||
number: number;
|
number: number;
|
||||||
id: string;
|
id: string;
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
|
updatedAt: number;
|
||||||
progress: ProgressItem;
|
progress: ProgressItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProgressMediaItem {
|
export interface ProgressMediaItem {
|
||||||
title: string;
|
title: string;
|
||||||
year: number;
|
year?: number;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
type: "show" | "movie";
|
type: "show" | "movie";
|
||||||
progress?: ProgressItem;
|
progress?: ProgressItem;
|
||||||
|
@ -43,9 +44,9 @@ export interface ProgressStore {
|
||||||
items: Record<string, ProgressMediaItem>;
|
items: Record<string, ProgressMediaItem>;
|
||||||
updateItem(ops: UpdateItemOptions): void;
|
updateItem(ops: UpdateItemOptions): void;
|
||||||
removeItem(id: string): void;
|
removeItem(id: string): void;
|
||||||
|
replaceItems(items: Record<string, ProgressMediaItem>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add migration from previous progress store
|
|
||||||
export const useProgressStore = create(
|
export const useProgressStore = create(
|
||||||
persist(
|
persist(
|
||||||
immer<ProgressStore>((set) => ({
|
immer<ProgressStore>((set) => ({
|
||||||
|
@ -55,6 +56,11 @@ export const useProgressStore = create(
|
||||||
delete s.items[id];
|
delete s.items[id];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
replaceItems(items: Record<string, ProgressMediaItem>) {
|
||||||
|
set((s) => {
|
||||||
|
s.items = items;
|
||||||
|
});
|
||||||
|
},
|
||||||
updateItem({ meta, progress }) {
|
updateItem({ meta, progress }) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
if (!s.items[meta.tmdbId])
|
if (!s.items[meta.tmdbId])
|
||||||
|
@ -95,6 +101,7 @@ export const useProgressStore = create(
|
||||||
number: meta.episode.number,
|
number: meta.episode.number,
|
||||||
title: meta.episode.title,
|
title: meta.episode.title,
|
||||||
seasonId: meta.season.tmdbId,
|
seasonId: meta.season.tmdbId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
progress: {
|
progress: {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
watched: 0,
|
watched: 0,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export interface MediaItem {
|
export interface MediaItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
year: number;
|
year?: number;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
type: "show" | "movie";
|
type: "show" | "movie";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue