Files
native-app/apps/expo/src/components/player/ScraperProcess.tsx
Adrian Castro c9222b0760 chore: cleanup
2024-03-06 20:21:25 +01:00

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>
);
};