mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 14:43:25 +00:00
improve loading, caption renderer, season/episode selector, source selector
This commit is contained in:
@@ -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";
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user