From 0aa9c9d8f79d8081a52d23e98260f96e4f35288b Mon Sep 17 00:00:00 2001
From: Adrian Castro <22133246+castdrian@users.noreply.github.com>
Date: Mon, 25 Mar 2024 20:20:07 +0100
Subject: [PATCH] feat: autoplay
---
apps/expo/src/app/videoPlayer.tsx | 14 ++--
.../src/components/player/ScraperProcess.tsx | 38 +++++++---
.../src/components/player/VideoPlayer.tsx | 29 +++++++-
apps/expo/src/lib/meta.ts | 70 +++++++++++++++++++
apps/expo/src/stores/player/slices/video.ts | 26 -------
5 files changed, 134 insertions(+), 43 deletions(-)
create mode 100644 apps/expo/src/lib/meta.ts
diff --git a/apps/expo/src/app/videoPlayer.tsx b/apps/expo/src/app/videoPlayer.tsx
index 2d00d3c..e6159ca 100644
--- a/apps/expo/src/app/videoPlayer.tsx
+++ b/apps/expo/src/app/videoPlayer.tsx
@@ -1,4 +1,6 @@
-import { useLocalSearchParams, useRouter } from "expo-router";
+import { useLocalSearchParams } from "expo-router";
+
+import type { ScrapeMedia } from "@movie-web/provider-utils";
import type { ItemData } from "~/components/item/item";
import { ScraperProcess } from "~/components/player/ScraperProcess";
@@ -12,15 +14,15 @@ export default function VideoPlayerWrapper() {
const asset = usePlayerStore((state) => state.asset);
const { presentFullscreenPlayer } = usePlayer();
- const router = useRouter();
const params = useLocalSearchParams();
const data = params.data
? (JSON.parse(params.data as string) as ItemData)
- : null;
+ : undefined;
+ const media = params.media
+ ? (JSON.parse(params.media as string) as ScrapeMedia)
+ : undefined;
const download = params.download === "true";
- if (!data) return router.back();
-
void presentFullscreenPlayer();
if (asset) {
@@ -32,7 +34,7 @@ export default function VideoPlayerWrapper() {
}
if (playerStatus === PlayerStatus.SCRAPING) {
- return ;
+ return ;
}
if (playerStatus === PlayerStatus.READY) {
diff --git a/apps/expo/src/components/player/ScraperProcess.tsx b/apps/expo/src/components/player/ScraperProcess.tsx
index 277754c..c4b798b 100644
--- a/apps/expo/src/components/player/ScraperProcess.tsx
+++ b/apps/expo/src/components/player/ScraperProcess.tsx
@@ -4,7 +4,11 @@ import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import { View } from "tamagui";
-import type { HlsBasedStream } from "@movie-web/provider-utils";
+import type {
+ HlsBasedStream,
+ RunOutput,
+ ScrapeMedia,
+} from "@movie-web/provider-utils";
import {
extractTracksFromHLS,
findHighestQuality,
@@ -12,21 +16,27 @@ import {
import type { ItemData } from "../item/item";
import type { AudioTrack } from "./AudioTrackSelector";
+import type { PlayerMeta } from "~/stores/player/slices/video";
import { useDownloadManager } from "~/hooks/DownloadManagerContext";
import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape";
+import { convertMetaToScrapeMedia } from "~/lib/meta";
import { constructFullUrl } from "~/lib/url";
import { PlayerStatus } from "~/stores/player/slices/interface";
-import { convertMetaToScrapeMedia } from "~/stores/player/slices/video";
import { usePlayerStore } from "~/stores/player/store";
import { ScrapeCard, ScrapeItem } from "./ScrapeCard";
interface ScraperProcessProps {
- data: ItemData;
+ data?: ItemData;
+ media?: ScrapeMedia;
download?: boolean;
}
-export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
+export const ScraperProcess = ({
+ data,
+ media,
+ download,
+}: ScraperProcessProps) => {
const router = useRouter();
const { startDownload } = useDownloadManager();
@@ -43,10 +53,19 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
useEffect(() => {
const fetchData = async () => {
- if (!data) return router.back();
- const meta = await convertMovieIdToMeta(data.id, data.type);
- if (!meta) return;
- const streamResult = await startScraping(convertMetaToScrapeMedia(meta));
+ if (!data && !media) return router.back();
+
+ let streamResult: RunOutput | null = null;
+ let meta: PlayerMeta | undefined = undefined;
+
+ if (!media && data) {
+ meta = await convertMovieIdToMeta(data.id, data.type);
+ 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) {
@@ -76,7 +95,7 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
if (tracks?.audio.length) {
const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({
uri: constructFullUrl(
- (streamResult.stream as HlsBasedStream).playlist,
+ (streamResult?.stream as HlsBasedStream).playlist,
track.uri,
),
name: track.properties[0]?.attributes.name?.toString() ?? "Unknown",
@@ -106,6 +125,7 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
convertMovieIdToMeta,
data,
download,
+ media,
router,
setAudioTracks,
setHlsTracks,
diff --git a/apps/expo/src/components/player/VideoPlayer.tsx b/apps/expo/src/components/player/VideoPlayer.tsx
index 3905dbb..1b38259 100644
--- a/apps/expo/src/components/player/VideoPlayer.tsx
+++ b/apps/expo/src/components/player/VideoPlayer.tsx
@@ -1,3 +1,4 @@
+import type { AVPlaybackStatus } from "expo-av";
import type { SharedValue } from "react-native-reanimated";
import { useEffect, useState } from "react";
import { Dimensions, Platform } from "react-native";
@@ -23,6 +24,7 @@ import { useBrightness } from "~/hooks/player/useBrightness";
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
import { usePlayer } from "~/hooks/player/usePlayer";
import { useVolume } from "~/hooks/player/useVolume";
+import { convertMetaToScrapeMedia, getNextEpisode } from "~/lib/meta";
import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store";
import { usePlayerSettingsStore } from "~/stores/settings";
@@ -61,8 +63,10 @@ export const VideoPlayer = () => {
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
const toggleAudio = usePlayerStore((state) => state.toggleAudio);
const toggleState = usePlayerStore((state) => state.toggleState);
+ const meta = usePlayerStore((state) => state.meta);
+ const setMeta = usePlayerStore((state) => state.setMeta);
- const { gestureControls } = usePlayerSettingsStore();
+ const { gestureControls, autoPlay } = usePlayerSettingsStore();
const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode);
@@ -212,6 +216,27 @@ export const VideoPlayer = () => {
}
};
+ const onPlaybackStatusUpdate = async (status: AVPlaybackStatus) => {
+ setStatus(status);
+ if (
+ status.isLoaded &&
+ status.didJustFinish &&
+ !status.isLooping &&
+ autoPlay
+ ) {
+ if (meta?.type !== "show") return;
+ const nextEpisodeMeta = await getNextEpisode(meta);
+ if (!nextEpisodeMeta) return;
+ setMeta(nextEpisodeMeta);
+ const media = convertMetaToScrapeMedia(nextEpisodeMeta);
+
+ router.replace({
+ pathname: "/videoPlayer",
+ params: { media: JSON.stringify(media) },
+ });
+ }
+ };
+
return (
{
rate={currentSpeed}
onLoadStart={onVideoLoadStart}
onReadyForDisplay={onReadyForDisplay}
- onPlaybackStatusUpdate={setStatus}
+ onPlaybackStatusUpdate={onPlaybackStatusUpdate}
style={[
{
position: "absolute",
diff --git a/apps/expo/src/lib/meta.ts b/apps/expo/src/lib/meta.ts
new file mode 100644
index 0000000..da1939a
--- /dev/null
+++ b/apps/expo/src/lib/meta.ts
@@ -0,0 +1,70 @@
+import type { ScrapeMedia } from "@movie-web/provider-utils";
+import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
+
+import type { PlayerMeta } from "~/stores/player/slices/video";
+
+export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
+ if (meta.type === "movie") {
+ return {
+ title: meta.title,
+ releaseYear: meta.releaseYear,
+ type: "movie",
+ tmdbId: meta.tmdbId,
+ imdbId: meta.imdbId,
+ };
+ }
+ if (meta.type === "show") {
+ return {
+ title: meta.title,
+ releaseYear: meta.releaseYear,
+ type: "show",
+ tmdbId: meta.tmdbId,
+ season: meta.season!,
+ episode: meta.episode!,
+ imdbId: meta.imdbId,
+ };
+ }
+ throw new Error("Invalid meta type");
+};
+
+export const getNextEpisode = async (
+ meta: PlayerMeta,
+): Promise => {
+ if (meta.type === "show") {
+ const currentEpisode = meta.episode!;
+ const nextEpisode = meta.episodes!.find(
+ (episode) => episode.number === currentEpisode.number + 1,
+ );
+ if (!nextEpisode) {
+ const media = await fetchMediaDetails(meta.tmdbId, "tv");
+ if (!media) return;
+
+ const nextSeason = media.result.seasons.find(
+ (season) => season.season_number === meta.season!.number + 1,
+ );
+ if (!nextSeason) return;
+ const seasonDetails = await fetchSeasonDetails(
+ meta.tmdbId,
+ nextSeason.season_number,
+ );
+ if (!seasonDetails) return;
+ return {
+ ...meta,
+ season: {
+ number: nextSeason.season_number,
+ tmdbId: meta.season!.tmdbId,
+ },
+ episode: {
+ number: seasonDetails.episodes[0]?.episode_number ?? 1,
+ tmdbId: seasonDetails.episodes[0]?.id.toString() ?? "",
+ title: seasonDetails.episodes[0]?.name,
+ },
+ };
+ }
+ return {
+ ...meta,
+ episode: nextEpisode,
+ };
+ }
+ throw new Error("Invalid meta type");
+};
diff --git a/apps/expo/src/stores/player/slices/video.ts b/apps/expo/src/stores/player/slices/video.ts
index 4e57f9b..437d269 100644
--- a/apps/expo/src/stores/player/slices/video.ts
+++ b/apps/expo/src/stores/player/slices/video.ts
@@ -1,8 +1,6 @@
import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av";
import type { Asset } from "expo-media-library";
-import type { ScrapeMedia } from "@movie-web/provider-utils";
-
import type { MakeSlice } from "./types";
import { PlayerStatus } from "./interface";
@@ -43,30 +41,6 @@ export interface VideoSlice {
resetVideo(): void;
}
-export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
- if (meta.type === "movie") {
- return {
- title: meta.title,
- releaseYear: meta.releaseYear,
- type: "movie",
- tmdbId: meta.tmdbId,
- imdbId: meta.imdbId,
- };
- }
- if (meta.type === "show") {
- return {
- title: meta.title,
- releaseYear: meta.releaseYear,
- type: "show",
- tmdbId: meta.tmdbId,
- season: meta.season!,
- episode: meta.episode!,
- imdbId: meta.imdbId,
- };
- }
- throw new Error("Invalid meta type");
-};
-
export const createVideoSlice: MakeSlice = (set) => ({
videoRef: null,
videoSrc: null,