mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 21:45:06 +01: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) {
|
||||
if (!obj) return undefined;
|
||||
if (obj.type !== "show") return;
|
||||
// TODO only show latest episode watched
|
||||
const ep = Object.values(obj.episodes)[0];
|
||||
const ep = Object.values(obj.episodes).sort(
|
||||
(a, b) => b.updatedAt - a.updatedAt
|
||||
)[0];
|
||||
const season = obj.seasons[ep?.seasonId];
|
||||
if (!ep || !season) return;
|
||||
return {
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
DisplayInterfaceEvents,
|
||||
} from "@/components/player/display/displayInterface";
|
||||
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
||||
import { getMediaErrorDetails } from "@/components/player/utils/mediaErrorDetails";
|
||||
import {
|
||||
LoadableSource,
|
||||
SourceQuality,
|
||||
@ -119,9 +120,12 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error("HLS error", data);
|
||||
if (data.fatal) {
|
||||
throw new Error(
|
||||
`HLS ERROR:${data.error?.message ?? "Something went wrong"}`
|
||||
);
|
||||
emit("error", {
|
||||
message: data.error.message,
|
||||
stackTrace: data.error.stack,
|
||||
errorName: data.error.name,
|
||||
type: "hls",
|
||||
});
|
||||
}
|
||||
});
|
||||
hls.on(Hls.Events.MANIFEST_LOADED, () => {
|
||||
@ -154,6 +158,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
emit("play", undefined);
|
||||
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("pause", () => emit("pause", undefined));
|
||||
videoElement.addEventListener("canplay", () => emit("loading", false));
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
||||
import { Listener } from "@/utils/events";
|
||||
|
||||
export type DisplayErrorType = "hls" | "htmlvideo";
|
||||
export type DisplayError = {
|
||||
stackTrace?: string;
|
||||
message: string;
|
||||
errorName: string;
|
||||
type: DisplayErrorType;
|
||||
};
|
||||
|
||||
export type DisplayInterfaceEvents = {
|
||||
play: void;
|
||||
pause: void;
|
||||
@ -15,6 +23,7 @@ export type DisplayInterfaceEvents = {
|
||||
needstrack: boolean;
|
||||
canairplay: boolean;
|
||||
playbackrate: number;
|
||||
error: DisplayError;
|
||||
};
|
||||
|
||||
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 { useQueryParam } from "@/hooks/useQueryParams";
|
||||
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
||||
import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart";
|
||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||
import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
|
||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||
@ -108,6 +109,7 @@ export function PlayerView() {
|
||||
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
||||
<ScrapeErrorPart data={errorData} />
|
||||
) : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null}
|
||||
</PlayerPart>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { Dropdown } from "@/components/Dropdown";
|
||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||
import { Title } from "@/components/text/Title";
|
||||
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
||||
import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart";
|
||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { SourceSliceSource, StreamType } from "@/stores/player/utils/qualities";
|
||||
@ -105,6 +106,7 @@ export default function VideoTesterView() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null}
|
||||
</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 { BookmarkStoreData } from "./types";
|
||||
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
||||
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>()
|
||||
.setKey("mw-bookmarks")
|
||||
.addVersion({
|
||||
@ -20,6 +28,28 @@ export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
||||
})
|
||||
.addVersion({
|
||||
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() {
|
||||
return {
|
||||
bookmarks: [],
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
|
||||
import { createVersionedStore } from "@/utils/storage";
|
||||
|
||||
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||
@ -28,6 +30,93 @@ export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||
})
|
||||
.addVersion({
|
||||
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() {
|
||||
return {
|
||||
items: [],
|
||||
|
@ -6,7 +6,7 @@ import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
|
||||
export interface BookmarkMediaItem {
|
||||
title: string;
|
||||
year: number;
|
||||
year?: number;
|
||||
poster?: string;
|
||||
type: "show" | "movie";
|
||||
updatedAt: number;
|
||||
@ -16,9 +16,9 @@ export interface ProgressStore {
|
||||
bookmarks: Record<string, BookmarkMediaItem>;
|
||||
addBookmark(meta: PlayerMeta): void;
|
||||
removeBookmark(id: string): void;
|
||||
replaceBookmarks(items: Record<string, BookmarkMediaItem>): void;
|
||||
}
|
||||
|
||||
// TODO add migration from previous bookmark store
|
||||
export const useBookmarkStore = create(
|
||||
persist(
|
||||
immer<ProgressStore>((set) => ({
|
||||
@ -39,6 +39,11 @@ export const useBookmarkStore = create(
|
||||
};
|
||||
});
|
||||
},
|
||||
replaceBookmarks(items: Record<string, BookmarkMediaItem>) {
|
||||
set((s) => {
|
||||
s.bookmarks = items;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::bookmarks",
|
||||
|
@ -90,6 +90,12 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
|
||||
s.mediaPlaying.playbackRate = rate;
|
||||
});
|
||||
});
|
||||
newDisplay.on("error", (err) => {
|
||||
set((s) => {
|
||||
s.status = playerStatus.PLAYBACK_ERROR;
|
||||
s.interface.error = err;
|
||||
});
|
||||
});
|
||||
|
||||
set((s) => {
|
||||
s.display = newDisplay;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DisplayError } from "@/components/player/display/displayInterface";
|
||||
import { MakeSlice } from "@/stores/player/slices/types";
|
||||
|
||||
export enum VideoPlayerTimeFormat {
|
||||
@ -23,6 +24,7 @@ export interface InterfaceSlice {
|
||||
isCasting: boolean;
|
||||
hideNextEpisodeBtn: boolean;
|
||||
shouldStartFromBeginning: boolean;
|
||||
error?: DisplayError;
|
||||
|
||||
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"
|
||||
|
@ -14,6 +14,7 @@ export const playerStatus = {
|
||||
SCRAPING: "scraping",
|
||||
PLAYING: "playing",
|
||||
SCRAPE_NOT_FOUND: "scrapeNotFound",
|
||||
PLAYBACK_ERROR: "playbackError",
|
||||
} as const;
|
||||
|
||||
export type PlayerStatus = ValuesOf<typeof playerStatus>;
|
||||
|
@ -20,12 +20,13 @@ export interface ProgressEpisodeItem {
|
||||
number: number;
|
||||
id: string;
|
||||
seasonId: string;
|
||||
updatedAt: number;
|
||||
progress: ProgressItem;
|
||||
}
|
||||
|
||||
export interface ProgressMediaItem {
|
||||
title: string;
|
||||
year: number;
|
||||
year?: number;
|
||||
poster?: string;
|
||||
type: "show" | "movie";
|
||||
progress?: ProgressItem;
|
||||
@ -43,9 +44,9 @@ export interface ProgressStore {
|
||||
items: Record<string, ProgressMediaItem>;
|
||||
updateItem(ops: UpdateItemOptions): void;
|
||||
removeItem(id: string): void;
|
||||
replaceItems(items: Record<string, ProgressMediaItem>): void;
|
||||
}
|
||||
|
||||
// TODO add migration from previous progress store
|
||||
export const useProgressStore = create(
|
||||
persist(
|
||||
immer<ProgressStore>((set) => ({
|
||||
@ -55,6 +56,11 @@ export const useProgressStore = create(
|
||||
delete s.items[id];
|
||||
});
|
||||
},
|
||||
replaceItems(items: Record<string, ProgressMediaItem>) {
|
||||
set((s) => {
|
||||
s.items = items;
|
||||
});
|
||||
},
|
||||
updateItem({ meta, progress }) {
|
||||
set((s) => {
|
||||
if (!s.items[meta.tmdbId])
|
||||
@ -95,6 +101,7 @@ export const useProgressStore = create(
|
||||
number: meta.episode.number,
|
||||
title: meta.episode.title,
|
||||
seasonId: meta.season.tmdbId,
|
||||
updatedAt: Date.now(),
|
||||
progress: {
|
||||
duration: 0,
|
||||
watched: 0,
|
||||
|
@ -1,7 +1,7 @@
|
||||
export interface MediaItem {
|
||||
id: string;
|
||||
title: string;
|
||||
year: number;
|
||||
year?: number;
|
||||
poster?: string;
|
||||
type: "show" | "movie";
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user