improve loading, caption renderer, season/episode selector, source selector

This commit is contained in:
Jorrin
2024-02-19 22:12:08 +01:00
parent efab11bff5
commit 90c6c2093b
31 changed files with 1453 additions and 824 deletions

View File

@@ -1,9 +1,5 @@
import type { ScrapeMedia, Stream } from "@movie-web/providers";
import { getBuiltinSources } from "@movie-web/providers";
export const name = "provider-utils";
export * from "./video";
export * from "./util";
export type { Stream, ScrapeMedia };
export { getBuiltinSources };
export * from "@movie-web/providers";

View File

@@ -2,9 +2,11 @@ import type { AppendToResponse, MovieDetails, TvShowDetails } from "tmdb-ts";
import type { ScrapeMedia } from "@movie-web/providers";
export function transformSearchResultToScrapeMedia(
type: "tv" | "movie",
result: TvShowDetails | MovieDetails,
export function transformSearchResultToScrapeMedia<T extends "tv" | "movie">(
type: T,
result: T extends "tv"
? AppendToResponse<TvShowDetails, "external_ids"[], "tvShow">
: AppendToResponse<MovieDetails, "external_ids"[], "movie">,
season?: number,
episode?: number,
): ScrapeMedia {

View File

@@ -4,10 +4,15 @@ import { default as toWebVTT } from "srt-webvtt";
import type {
EmbedOutput,
EmbedRunnerOptions,
FileBasedStream,
FullScraperEvents,
Qualities,
RunnerOptions,
RunOutput,
ScrapeMedia,
SourcererOutput,
SourceRunnerOptions,
Stream,
} from "@movie-web/providers";
import {
@@ -44,105 +49,173 @@ export type RunnerEvent =
| UpdateEvent
| DiscoverEmbedsEvent;
export const providers = makeProviders({
fetcher: makeStandardFetcher(fetch),
target: targets.NATIVE,
consistentIpForRequests: true,
});
export async function getVideoStream({
sourceId,
media,
forceVTT,
onEvent,
events,
}: {
sourceId?: string;
media: ScrapeMedia;
forceVTT?: boolean;
onEvent?: (event: RunnerEvent) => void;
}): Promise<Stream | null> {
const providers = makeProviders({
fetcher: makeStandardFetcher(fetch),
target: targets.NATIVE,
consistentIpForRequests: true,
});
events?: FullScraperEvents;
}): Promise<RunOutput | null> {
const options: RunnerOptions = {
media,
events: {
init: onEvent,
update: onEvent,
discoverEmbeds: onEvent,
start: onEvent,
},
events,
};
let stream: Stream | null = null;
if (sourceId) {
onEvent && onEvent({ sourceIds: [sourceId] });
let embedOutput: EmbedOutput | undefined;
const sourceResult = await providers
.runSourceScraper({
id: sourceId,
media,
})
.catch((error: Error) => {
onEvent &&
onEvent({ id: sourceId, percentage: 0, status: "failure", error });
return undefined;
});
if (sourceResult) {
onEvent && onEvent({ id: sourceId, percentage: 50, status: "pending" });
for (const embed of sourceResult.embeds) {
const embedResult = await providers
.runEmbedScraper({
id: embed.embedId,
url: embed.url,
})
.catch(() => undefined);
if (embedResult) {
embedOutput = embedResult;
onEvent &&
onEvent({ id: embed.embedId, percentage: 100, status: "success" });
}
}
}
if (embedOutput) {
stream = embedOutput.stream[0] ?? null;
} else if (sourceResult) {
stream = sourceResult.stream?.[0] ?? null;
}
if (stream) {
onEvent && onEvent({ id: sourceId, percentage: 100, status: "success" });
} else {
onEvent && onEvent({ id: sourceId, percentage: 100, status: "notfound" });
}
} else {
stream = await providers
.runAll(options)
.then((result) => result?.stream ?? null);
}
const stream = await providers.runAll(options);
if (!stream) return null;
if (forceVTT) {
if (stream.captions && stream.captions.length > 0) {
for (const caption of stream.captions) {
if (caption.type === "srt") {
const response = await fetch(caption.url);
const srtSubtitle = await response.blob();
const vttSubtitleUrl = await toWebVTT(srtSubtitle);
caption.url = vttSubtitleUrl;
caption.type = "vtt";
}
}
}
const streamResult = await convertStreamCaptionsToWebVTT(stream.stream);
return { ...stream, stream: streamResult };
}
return stream;
}
export async function getVideoStreamFromSource({
sourceId,
media,
events,
}: {
sourceId: string;
media: ScrapeMedia;
events?: SourceRunnerOptions["events"];
}): Promise<SourcererOutput> {
const sourceResult = await providers.runSourceScraper({
id: sourceId,
media,
events,
});
return sourceResult;
}
export async function getVideoStreamFromEmbed({
embedId,
url,
events,
}: {
embedId: string;
url: string;
events?: EmbedRunnerOptions["events"];
}): Promise<EmbedOutput> {
const embedResult = await providers.runEmbedScraper({
id: embedId,
url,
events,
});
return embedResult;
}
// export async function getVideoStream({
// sourceId,
// media,
// forceVTT,
// onEvent,
// }: {
// sourceId?: string;
// media: ScrapeMedia;
// forceVTT?: boolean;
// onEvent?: (event: RunnerEvent) => void;
// }): Promise<Stream | null> {
// const providers = makeProviders({
// fetcher: makeStandardFetcher(fetch),
// target: targets.NATIVE,
// consistentIpForRequests: true,
// });
// const options: RunnerOptions = {
// media,
// events: {
// init: onEvent,
// update: onEvent,
// discoverEmbeds: onEvent,
// start: onEvent,
// },
// };
// let stream: Stream | null = null;
// if (sourceId) {
// onEvent && onEvent({ sourceIds: [sourceId] });
// let embedOutput: EmbedOutput | undefined;
// const sourceResult = await providers
// .runSourceScraper({
// id: sourceId,
// media,
// events: {},
// })
// .catch((error: Error) => {
// onEvent &&
// onEvent({ id: sourceId, percentage: 0, status: "failure", error });
// return undefined;
// });
// if (sourceResult) {
// onEvent && onEvent({ id: sourceId, percentage: 50, status: "pending" });
// for (const embed of sourceResult.embeds) {
// const embedResult = await providers
// .runEmbedScraper({
// id: embed.embedId,
// url: embed.url,
// })
// .catch(() => undefined);
// if (embedResult) {
// embedOutput = embedResult;
// onEvent &&
// onEvent({ id: embed.embedId, percentage: 100, status: "success" });
// }
// }
// }
// if (embedOutput) {
// stream = embedOutput.stream[0] ?? null;
// } else if (sourceResult) {
// stream = sourceResult.stream?.[0] ?? null;
// }
// if (stream) {
// onEvent && onEvent({ id: sourceId, percentage: 100, status: "success" });
// } else {
// onEvent && onEvent({ id: sourceId, percentage: 100, status: "notfound" });
// }
// } else {
// stream = await providers
// .runAll(options)
// .then((result) => result?.stream ?? null);
// }
// if (!stream) return null;
// if (forceVTT) {
// if (stream.captions && stream.captions.length > 0) {
// for (const caption of stream.captions) {
// if (caption.type === "srt") {
// const response = await fetch(caption.url);
// const srtSubtitle = await response.blob();
// const vttSubtitleUrl = await toWebVTT(srtSubtitle);
// caption.url = vttSubtitleUrl;
// caption.type = "vtt";
// }
// }
// }
// }
// return stream;
// }
export function findHighestQuality(
stream: FileBasedStream,
): Qualities | undefined {
@@ -186,3 +259,18 @@ export async function extractTracksFromHLS(
return null;
}
}
export async function convertStreamCaptionsToWebVTT(
stream: Stream,
): Promise<Stream> {
if (!stream.captions) return stream;
for (const caption of stream.captions) {
if (caption.type === "srt") {
const response = await fetch(caption.url);
const srt = await response.blob();
caption.url = await toWebVTT(srt);
caption.type = "vtt";
}
}
return stream;
}

View File

@@ -1,26 +1,34 @@
import type { MovieDetails, SeasonDetails, TvShowDetails } from "tmdb-ts";
import type {
AppendToResponse,
MovieDetails,
SeasonDetails,
TvShowDetails,
} from "tmdb-ts";
import { tmdb } from "./util";
export async function fetchMediaDetails(
id: string,
type: "movie" | "tv",
): Promise<
{ type: "movie" | "tv"; result: TvShowDetails | MovieDetails } | undefined
> {
try {
const result =
type === "movie"
? await tmdb.movies.details(parseInt(id, 10), ["external_ids"])
: await tmdb.tvShows.details(parseInt(id, 10), ["external_ids"]);
return {
type,
result,
};
} catch (ex) {
return undefined;
export async function fetchMediaDetails<
T extends "movie" | "tv",
R = T extends "movie"
? {
type: "movie";
result: AppendToResponse<MovieDetails, "external_ids"[], "movie">;
}
: {
type: "tv";
result: AppendToResponse<TvShowDetails, "external_ids"[], "tvShow">;
},
>(id: string, type: T): Promise<R | undefined> {
if (type === "movie") {
const movieResult = await tmdb.movies.details(parseInt(id, 10), [
"external_ids",
]);
return { type: "movie", result: movieResult } as R;
}
const tvResult = await tmdb.tvShows.details(parseInt(id, 10), [
"external_ids",
]);
return { type: "tv", result: tvResult } as R;
}
export async function fetchSeasonDetails(