mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 18:13:25 +00:00
222 lines
6.9 KiB
TypeScript
222 lines
6.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { ActivityIndicator, View } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
|
|
import type {
|
|
DiscoverEmbedsEvent,
|
|
HlsBasedStream,
|
|
InitEvent,
|
|
RunnerEvent,
|
|
UpdateEvent,
|
|
} from "@movie-web/provider-utils";
|
|
import {
|
|
extractTracksFromHLS,
|
|
getVideoStream,
|
|
transformSearchResultToScrapeMedia,
|
|
} from "@movie-web/provider-utils";
|
|
import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
|
|
|
|
import type { ItemData } from "../item/item";
|
|
import type { AudioTrack } from "./AudioTrackSelector";
|
|
import { PlayerStatus } from "~/stores/player/slices/interface";
|
|
import { usePlayerStore } from "~/stores/player/store";
|
|
import { Text } from "../ui/Text";
|
|
|
|
interface ScraperProcessProps {
|
|
data: ItemData;
|
|
}
|
|
|
|
enum ScrapeStatus {
|
|
LOADING = "loading",
|
|
SUCCESS = "success",
|
|
ERROR = "error",
|
|
}
|
|
|
|
export const ScraperProcess = ({ data }: ScraperProcessProps) => {
|
|
const router = useRouter();
|
|
|
|
const meta = usePlayerStore((state) => state.meta);
|
|
const setStream = usePlayerStore((state) => state.setCurrentStream);
|
|
const setSeasonData = usePlayerStore((state) => state.setSeasonData);
|
|
const setHlsTracks = usePlayerStore((state) => state.setHlsTracks);
|
|
const setAudioTracks = usePlayerStore((state) => state.setAudioTracks);
|
|
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
|
|
const setSourceId = usePlayerStore((state) => state.setSourceId);
|
|
const setMeta = usePlayerStore((state) => state.setMeta);
|
|
const [checkedSource, setCheckedSource] = useState("");
|
|
|
|
function isInitEvent(event: RunnerEvent): event is InitEvent {
|
|
return (event as InitEvent).sourceIds !== undefined;
|
|
}
|
|
|
|
function isUpdateEvent(event: RunnerEvent): event is UpdateEvent {
|
|
return (event as UpdateEvent).percentage !== undefined;
|
|
}
|
|
|
|
function isDiscoverEmbedsEvent(
|
|
event: RunnerEvent,
|
|
): event is DiscoverEmbedsEvent {
|
|
return (event as DiscoverEmbedsEvent).sourceId !== undefined;
|
|
}
|
|
|
|
const handleEvent = useCallback((event: RunnerEvent) => {
|
|
if (typeof event === "string") {
|
|
setCheckedSource(event);
|
|
setScrapeStatus({ status: ScrapeStatus.LOADING, progress: 10 });
|
|
} else if (isUpdateEvent(event)) {
|
|
switch (event.status) {
|
|
case ScrapeStatus.SUCCESS:
|
|
setScrapeStatus({ status: ScrapeStatus.SUCCESS, progress: 100 });
|
|
break;
|
|
case ScrapeStatus.ERROR as string:
|
|
setScrapeStatus({ status: ScrapeStatus.ERROR, progress: 0 });
|
|
break;
|
|
case ScrapeStatus.LOADING as string:
|
|
}
|
|
setCheckedSource(event.id);
|
|
} else if (isInitEvent(event) || isDiscoverEmbedsEvent(event)) {
|
|
setScrapeStatus((prevStatus) => ({
|
|
status: ScrapeStatus.LOADING,
|
|
progress: Math.min(prevStatus.progress + 20, 95),
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
const [_scrapeStatus, setScrapeStatus] = useState({
|
|
status: ScrapeStatus.LOADING,
|
|
progress: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (!data) return router.back();
|
|
const media = await fetchMediaDetails(data.id, data.type);
|
|
if (!media) return router.back();
|
|
const scrapeMedia = transformSearchResultToScrapeMedia(
|
|
media.type,
|
|
media.result,
|
|
meta?.season?.number,
|
|
meta?.episode?.number,
|
|
);
|
|
let seasonData = null;
|
|
if (scrapeMedia.type === "show") {
|
|
seasonData = await fetchSeasonDetails(
|
|
scrapeMedia.tmdbId,
|
|
scrapeMedia.season.number,
|
|
);
|
|
}
|
|
|
|
setMeta({
|
|
...scrapeMedia,
|
|
poster: media.result.poster_path,
|
|
...("season" in scrapeMedia
|
|
? {
|
|
season: {
|
|
number: scrapeMedia.season.number,
|
|
tmdbId: scrapeMedia.tmdbId,
|
|
},
|
|
episode: {
|
|
number: scrapeMedia.episode.number,
|
|
tmdbId: scrapeMedia.episode.tmdbId,
|
|
},
|
|
episodes:
|
|
seasonData?.episodes.map((e) => ({
|
|
tmdbId: e.id.toString(),
|
|
number: e.episode_number,
|
|
name: e.name,
|
|
})) ?? [],
|
|
}
|
|
: {}),
|
|
});
|
|
const streamResult = await getVideoStream({
|
|
media: scrapeMedia,
|
|
events: {
|
|
// init: handleEvent,
|
|
update: handleEvent,
|
|
discoverEmbeds: handleEvent,
|
|
start: handleEvent,
|
|
},
|
|
});
|
|
if (!streamResult) return router.back();
|
|
setStream(streamResult.stream);
|
|
|
|
if (streamResult.stream.type === "hls") {
|
|
const tracks = await extractTracksFromHLS(
|
|
streamResult.stream.playlist, // multiple audio tracks: 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8',
|
|
{
|
|
...streamResult.stream.preferredHeaders,
|
|
...streamResult.stream.headers,
|
|
},
|
|
);
|
|
if (tracks) setHlsTracks(tracks);
|
|
|
|
const constructFullUrl = (playlistUrl: string, uri: string) => {
|
|
const baseUrl = playlistUrl.substring(
|
|
0,
|
|
playlistUrl.lastIndexOf("/") + 1,
|
|
);
|
|
return uri.startsWith("http://") || uri.startsWith("https://")
|
|
? uri
|
|
: baseUrl + uri;
|
|
};
|
|
|
|
if (tracks?.audio.length) {
|
|
const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({
|
|
uri: constructFullUrl(
|
|
(streamResult.stream as HlsBasedStream).playlist,
|
|
track.uri,
|
|
),
|
|
name: (track.properties[0]?.attributes.name as string) ?? "Unknown",
|
|
language:
|
|
(track.properties[0]?.attributes.language as string) ?? "Unknown",
|
|
active:
|
|
(track.properties[0]?.attributes.default as boolean) ?? false,
|
|
}));
|
|
|
|
const uniqueTracks = new Set(audioTracks.map((t) => t.language));
|
|
|
|
const filteredAudioTracks = audioTracks.filter((track) => {
|
|
if (uniqueTracks.has(track.language)) {
|
|
uniqueTracks.delete(track.language);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
setAudioTracks(filteredAudioTracks);
|
|
}
|
|
}
|
|
setPlayerStatus(PlayerStatus.READY);
|
|
setSourceId(streamResult.sourceId);
|
|
};
|
|
void fetchData();
|
|
}, [
|
|
data,
|
|
router,
|
|
setHlsTracks,
|
|
setSeasonData,
|
|
setStream,
|
|
setPlayerStatus,
|
|
setSourceId,
|
|
setMeta,
|
|
meta?.season?.number,
|
|
meta?.episode?.number,
|
|
setAudioTracks,
|
|
handleEvent,
|
|
]);
|
|
|
|
return (
|
|
<View className="flex-1">
|
|
<View className="flex-1 items-center justify-center bg-black">
|
|
<View className="flex flex-col items-center">
|
|
<Text className="mb-4 text-2xl text-white">
|
|
Checking {checkedSource}
|
|
</Text>
|
|
<ActivityIndicator size="large" color="#0000ff" />
|
|
{/* <StatusCircle type={scrapeStatus.status} percentage={scrapeStatus.progress} /> */}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|