Files
native-app/apps/expo/src/components/player/ScraperProcess.tsx
2024-04-08 18:36:44 +02:00

238 lines
7.2 KiB
TypeScript

import { useEffect, useRef } from "react";
import { SafeAreaView } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import { View } from "tamagui";
import type {
HlsBasedStream,
RunOutput,
ScrapeMedia,
} from "@movie-web/provider-utils";
import {
constructFullUrl,
extractTracksFromHLS,
findHighestQuality,
} from "@movie-web/provider-utils";
import type { ItemData } from "../item/item";
import type { AudioTrack } from "./AudioTrackSelector";
import type { PlayerMeta } from "~/stores/player/slices/video";
import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape";
import { useDownloadManager } from "~/hooks/useDownloadManager";
import { convertMetaToScrapeMedia } from "~/lib/meta";
import { PlayerStatus } from "~/stores/player/slices/interface";
import { usePlayerStore } from "~/stores/player/store";
import { BackButton } from "./BackButton";
import { ScrapeCard, ScrapeItem } from "./ScrapeCard";
interface ScraperProcessProps {
data?: Partial<ItemData>;
media?: ScrapeMedia;
download?: boolean;
}
export const ScraperProcess = ({
data,
media,
download,
}: ScraperProcessProps) => {
const router = useRouter();
const { startDownload } = useDownloadManager();
const scrollViewRef = useRef<ScrollView>(null);
const { convertIdToMeta } = useMeta();
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
const setStream = usePlayerStore((state) => state.setCurrentStream);
const setHlsTracks = usePlayerStore((state) => state.setHlsTracks);
const setAudioTracks = usePlayerStore((state) => state.setAudioTracks);
const setPlayerStatus = usePlayerStore((state) => state.setPlayerStatus);
const setSourceId = usePlayerStore((state) => state.setSourceId);
useEffect(() => {
const fetchData = async () => {
if (!data?.id && !media) return router.back();
let streamResult: RunOutput | null = null;
let meta: PlayerMeta | undefined = undefined;
if (!media && data?.id && data.type) {
meta = await convertIdToMeta(
data.id,
data.type,
data.season,
data.episode,
);
if (!meta) return router.back();
}
const scrapeMedia = media ?? (meta && convertMetaToScrapeMedia(meta));
if (!scrapeMedia) return router.back();
streamResult = await startScraping(scrapeMedia);
if (!streamResult) return router.back();
if (download) {
if (streamResult.stream.type === "file") {
const highestQuality = findHighestQuality(streamResult.stream);
const url = highestQuality
? streamResult.stream.qualities[highestQuality]?.url
: null;
if (!url) return;
startDownload(url, "mp4", scrapeMedia).catch(console.error);
} else if (streamResult.stream.type === "hls") {
startDownload(streamResult.stream.playlist, "hls", scrapeMedia).catch(
console.error,
);
}
return router.back();
}
setStream(streamResult.stream);
if (streamResult.stream.type === "hls") {
const tracks = await extractTracksFromHLS(
streamResult.stream.playlist,
{
...streamResult.stream.preferredHeaders,
...streamResult.stream.headers,
},
);
if (tracks) setHlsTracks(tracks);
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?.toString() ?? "Unknown",
language:
track.properties[0]?.attributes.language?.toString() ?? "Unknown",
active: Boolean(track.properties[0]?.attributes.default) ?? 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();
}, [
convertIdToMeta,
data,
download,
media,
router,
setAudioTracks,
setHlsTracks,
setPlayerStatus,
setSourceId,
setStream,
startDownload,
startScraping,
]);
let currentProviderIndex = sourceOrder.findIndex(
(s) => s.id === currentSource || s.children.includes(currentSource ?? ""),
);
if (currentProviderIndex === -1) {
currentProviderIndex = sourceOrder.length - 1;
}
useEffect(() => {
scrollViewRef.current?.scrollTo({
y: currentProviderIndex * 110,
animated: true,
});
}, [currentProviderIndex]);
return (
<SafeAreaView
style={{
display: "flex",
height: "100%",
flexDirection: "column",
flex: 1,
}}
>
<View
flex={1}
alignItems="center"
justifyContent="center"
backgroundColor="$screenBackground"
>
<View position="absolute" top={40} left={40}>
<BackButton />
</View>
<ScrollView
ref={scrollViewRef}
contentContainerStyle={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
paddingVertical: 64,
}}
>
{sourceOrder.map((order) => {
const source = sources[order.id];
if (!source) return null;
const distance = Math.abs(
sourceOrder.findIndex((o) => o.id === order.id) -
currentProviderIndex,
);
return (
<View
key={order.id}
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
>
<ScrapeCard
id={order.id}
name={source.name}
status={source.status}
hasChildren={order.children.length > 0}
percentage={source.percentage}
>
<View
marginTop={order.children.length > 0 ? 8 : 0}
flexDirection="column"
gap={16}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
if (!embed) return null;
return (
<ScrapeItem
id={embedId}
name={embed.name}
status={embed.status}
percentage={embed.percentage}
key={embedId}
/>
);
})}
</View>
</ScrapeCard>
</View>
);
})}
</ScrollView>
</View>
</SafeAreaView>
);
};