diff --git a/apps/expo/app.config.ts b/apps/expo/app.config.ts index 9fd8780..9c17ebf 100644 --- a/apps/expo/app.config.ts +++ b/apps/expo/app.config.ts @@ -19,7 +19,7 @@ const defineConfig = (): ExpoConfig => ({ ios: { bundleIdentifier: "dev.movieweb.app", supportsTablet: true, - requireFullScreen: true, + requireFullScreen: true, }, android: { package: "dev.movieweb.app", @@ -41,13 +41,15 @@ const defineConfig = (): ExpoConfig => ({ tsconfigPaths: true, typedRoutes: true, }, - plugins: ["expo-router", [ - "expo-screen-orientation", - { - initialOrientation: "DEFAULT" - } - ] -], + plugins: [ + "expo-router", + [ + "expo-screen-orientation", + { + initialOrientation: "DEFAULT", + }, + ], + ], }); export default defineConfig; diff --git a/apps/expo/index.js b/apps/expo/index.js index 63e531c..ab16fb5 100644 --- a/apps/expo/index.js +++ b/apps/expo/index.js @@ -1 +1,2 @@ -import "expo-router/entry"; \ No newline at end of file +import "expo-router/entry"; +import "@react-native-anywhere/polyfill-base64"; diff --git a/apps/expo/package.json b/apps/expo/package.json index b5f3093..aee39b1 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -19,7 +19,10 @@ }, "dependencies": { "@expo/metro-config": "^0.17.3", + "@movie-web/provider-utils": "*", "@movie-web/tmdb": "*", + "@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0", + "@react-navigation/native": "^6.1.9", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo": "~50.0.5", diff --git a/apps/expo/src/app/components/item/item.tsx b/apps/expo/src/app/components/item/item.tsx index a13ea8f..e0649be 100644 --- a/apps/expo/src/app/components/item/item.tsx +++ b/apps/expo/src/app/components/item/item.tsx @@ -1,5 +1,12 @@ +import { Image, TouchableOpacity, View } from "react-native"; import { useRouter } from "expo-router"; -import { Image, View, TouchableOpacity } from "react-native"; +import * as ScreenOrientation from "expo-screen-orientation"; + +import { + getVideoUrl, + transformSearchResultToScrapeMedia, +} from "@movie-web/provider-utils"; +import { fetchMediaDetails } from "@movie-web/tmdb"; import { Text } from "~/components/ui/Text"; @@ -13,38 +20,67 @@ export interface ItemData { export default function Item({ data }: { data: ItemData }) { const router = useRouter(); - const { title, type, year, posterUrl } = data; + const { id, title, type, year, posterUrl } = data; - const handlePress = () => { - router.push('/video-player'); - // router.push({ - // pathname: '/video-player', - // params: { videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4' } - // }); + const handlePress = async () => { + router.push("/video-player"); + + const media = await fetchMediaDetails(id, type); + if (!media) return; + + const { result } = media; + let season: number | undefined; + let episode: number | undefined; + + if (type === "tv") { + // season = ?? undefined; + // episode = ?? undefined; + } + + const scrapeMedia = transformSearchResultToScrapeMedia( + type, + result, + season, + episode, + ); + + const videoUrl = await getVideoUrl(scrapeMedia); + if (!videoUrl) { + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ); + return router.push("/(tabs)"); + } + console.log(videoUrl); + + router.push({ + pathname: "/video-player", + params: { videoUrl }, + }); }; return ( - + { - - - - - {title} - - - {type === "tv" ? "Show" : "Movie"} - - - {year} - - - } + + + + + {title} + + + {type === "tv" ? "Show" : "Movie"} + + + {year} + + + } ); } diff --git a/apps/expo/src/app/video-player.tsx b/apps/expo/src/app/video-player.tsx index afca0a4..7652df3 100644 --- a/apps/expo/src/app/video-player.tsx +++ b/apps/expo/src/app/video-player.tsx @@ -1,10 +1,10 @@ -import React, { Component } from 'react'; -import { StyleSheet, ActivityIndicator } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import type { VideoRef } from 'react-native-video'; -import Video from 'react-native-video'; -import * as ScreenOrientation from 'expo-screen-orientation'; +import type { VideoRef } from "react-native-video"; +import React, { Component } from "react"; +import { ActivityIndicator, StyleSheet } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import Video from "react-native-video"; import { useLocalSearchParams } from "expo-router"; +import * as ScreenOrientation from "expo-screen-orientation"; interface VideoPlayerState { videoUrl: string; @@ -14,53 +14,57 @@ interface VideoPlayerState { } interface VideoPlayerProps { - videoUrl: string; + videoUrl: string; } export default function VideoPlayerWrapper() { - const params = useLocalSearchParams(); - const videoUrl = typeof params.videoUrl === 'string' ? params.videoUrl : ''; - return ; - } + const params = useLocalSearchParams(); + const videoUrl = typeof params.videoUrl === "string" ? params.videoUrl : ""; + return ; +} - class VideoPlayer extends Component { - private videoPlayer: React.RefObject; +class VideoPlayer extends Component { + private videoPlayer: React.RefObject; - constructor(props: VideoPlayerProps) { + constructor(props: VideoPlayerProps) { super(props); this.state = { - videoUrl: props.videoUrl || '', + videoUrl: props.videoUrl || "", fullscreen: true, isLoading: true, - paused: false + paused: false, }; this.videoPlayer = React.createRef(); } componentDidMount() { - const lockOrientation = async () => { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - }; - - const { videoUrl } = this.props; - - this.setState({ videoUrl }, () => { - if (this.videoPlayer.current) { - this.videoPlayer.current.presentFullscreenPlayer(); - void lockOrientation(); - } - }); + const lockOrientation = async () => { + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.LANDSCAPE, + ); + }; + + const { videoUrl } = this.props; + + this.setState({ videoUrl }, () => { + if (this.videoPlayer.current) { + this.videoPlayer.current.presentFullscreenPlayer(); + void lockOrientation(); + } + }); } componentWillUnmount() { - const unlockOrientation = async () => { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); - } + const unlockOrientation = async () => { + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ); + }; - if (this.videoPlayer.current) { - this.videoPlayer.current.dismissFullscreenPlayer(); - } - void unlockOrientation(); + if (this.videoPlayer.current) { + this.videoPlayer.current.dismissFullscreenPlayer(); + } + void unlockOrientation(); } onVideoLoadStart = () => { @@ -71,9 +75,9 @@ export default function VideoPlayerWrapper() { this.setState({ isLoading: false }); }; -// onVideoError = () => { // probably useful later -// console.log("Video playback error"); -// }; + // onVideoError = () => { // probably useful later + // console.log("Video playback error"); + // }; render() { return ( @@ -84,7 +88,7 @@ export default function VideoPlayerWrapper() { style={styles.fullScreen} fullscreen={this.state.fullscreen} paused={this.state.paused} - controls={true} + controls={true} onLoadStart={this.onVideoLoadStart} onReadyForDisplay={this.onReadyForDisplay} // onError={this.onVideoError} @@ -98,18 +102,18 @@ export default function VideoPlayerWrapper() { } const styles = StyleSheet.create({ + // taken from example, probably needs to be nativewind stuff instead container: { flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'black', + justifyContent: "center", + alignItems: "center", + backgroundColor: "black", }, fullScreen: { - position: 'absolute', + position: "absolute", top: 0, left: 0, bottom: 0, right: 0, }, - }); diff --git a/packages/provider-utils/package.json b/packages/provider-utils/package.json index 533f185..01abe92 100644 --- a/packages/provider-utils/package.json +++ b/packages/provider-utils/package.json @@ -29,6 +29,7 @@ }, "prettier": "@movie-web/prettier-config", "dependencies": { - "@movie-web/providers": "^2.1.1" + "@movie-web/providers": "^2.1.1", + "tmdb-ts": "^1.6.1" } } diff --git a/packages/provider-utils/src/index.ts b/packages/provider-utils/src/index.ts index bb356a3..c59ca8d 100644 --- a/packages/provider-utils/src/index.ts +++ b/packages/provider-utils/src/index.ts @@ -1 +1,3 @@ export const name = "provider-utils"; +export * from "./video"; +export * from "./util"; diff --git a/packages/provider-utils/src/util.ts b/packages/provider-utils/src/util.ts new file mode 100644 index 0000000..2b68eaf --- /dev/null +++ b/packages/provider-utils/src/util.ts @@ -0,0 +1,43 @@ +import type { MovieDetails, TvShowDetails } from "tmdb-ts"; + +import type { ScrapeMedia } from "@movie-web/providers"; + +export function transformSearchResultToScrapeMedia( + type: "tv" | "movie", + result: TvShowDetails | MovieDetails, + season?: number, + episode?: number, +): ScrapeMedia { + if (type === "tv") { + const tvResult = result as TvShowDetails; + return { + type: "show", + tmdbId: tvResult.id.toString(), + title: tvResult.name, + releaseYear: new Date(tvResult.first_air_date).getFullYear(), + season: { + number: season ?? tvResult.seasons[0]?.season_number ?? 1, + tmdbId: season + ? tvResult.seasons + .find((s) => s.season_number === season) + ?.id.toString() ?? "" + : tvResult.seasons[0]?.id.toString() ?? "", + }, + episode: { + number: episode ?? 1, + tmdbId: "", + }, + }; + } + if (type === "movie") { + const movieResult = result as MovieDetails; + return { + type: "movie", + tmdbId: movieResult.id.toString(), + title: movieResult.title, + releaseYear: new Date(movieResult.release_date).getFullYear(), + }; + } + + throw new Error("Invalid type parameter"); +} diff --git a/packages/provider-utils/src/video.ts b/packages/provider-utils/src/video.ts index 162a7a9..d483924 100644 --- a/packages/provider-utils/src/video.ts +++ b/packages/provider-utils/src/video.ts @@ -1,47 +1,57 @@ import type { - FileBasedStream, - Qualities, - RunnerOptions, - ScrapeMedia} from '@movie-web/providers'; + FileBasedStream, + Qualities, + RunnerOptions, + ScrapeMedia, +} from "@movie-web/providers"; import { - makeProviders, - makeStandardFetcher, - targets, - } from '@movie-web/providers'; + makeProviders, + makeStandardFetcher, + targets, +} from "@movie-web/providers"; -export async function getVideoUrl(media: ScrapeMedia): Promise { - const providers = makeProviders({ - fetcher: makeStandardFetcher(fetch), - target: targets.NATIVE, - consistentIpForRequests: true, - }); +export async function getVideoUrl(media: ScrapeMedia): Promise { + const providers = makeProviders({ + fetcher: makeStandardFetcher(fetch), + target: targets.NATIVE, + consistentIpForRequests: true, + }); - const options: RunnerOptions = { - media - }; + const options: RunnerOptions = { + media, + }; - const results = await providers.runAll(options); - if (!results) return null; + const results = await providers.runAll(options); + if (!results) return null; - let highestQuality; - let url; - - switch (results.stream.type) { - case 'file': - highestQuality = findHighestQuality(results.stream); - url = highestQuality ? results.stream.qualities[highestQuality]?.url : null; - return url ?? null; - case 'hls': - return results.stream.playlist; - } + let highestQuality; + let url; + + switch (results.stream.type) { + case "file": + highestQuality = findHighestQuality(results.stream); + url = highestQuality + ? results.stream.qualities[highestQuality]?.url + : null; + return url ?? null; + case "hls": + return results.stream.playlist; + } } function findHighestQuality(stream: FileBasedStream): Qualities | undefined { - const qualityOrder: Qualities[] = ['4k', '1080', '720', '480', '360', 'unknown']; - for (const quality of qualityOrder) { - if (stream.qualities[quality]) { - return quality; - } - } - return undefined; -} \ No newline at end of file + const qualityOrder: Qualities[] = [ + "4k", + "1080", + "720", + "480", + "360", + "unknown", + ]; + for (const quality of qualityOrder) { + if (stream.qualities[quality]) { + return quality; + } + } + return undefined; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9775945..7ae733e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,9 +34,18 @@ importers: '@expo/metro-config': specifier: ^0.17.3 version: 0.17.3(@react-native/babel-preset@0.73.20) + '@movie-web/provider-utils': + specifier: '*' + version: link:../../packages/provider-utils '@movie-web/tmdb': specifier: '*' version: link:../../packages/tmdb + '@react-native-anywhere/polyfill-base64': + specifier: 0.0.1-alpha.0 + version: 0.0.1-alpha.0 + '@react-navigation/native': + specifier: ^6.1.9 + version: 6.1.9(react-native@0.73.2)(react@18.2.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -149,6 +158,9 @@ importers: '@movie-web/providers': specifier: ^2.1.1 version: 2.1.1 + tmdb-ts: + specifier: ^1.6.1 + version: 1.6.1 devDependencies: '@movie-web/eslint-config': specifier: workspace:^0.2.0 @@ -2387,6 +2399,12 @@ packages: react: 18.2.0 dev: false + /@react-native-anywhere/polyfill-base64@0.0.1-alpha.0: + resolution: {integrity: sha512-OF3idcETV622AyFvvK54ot2EG0G43tZTZJyWtFHtrEKUmoUvSuC5DOMeLino0TwBQJn2s26MBnIPVgokBJb/xw==} + dependencies: + base-64: 0.1.0 + dev: false + /@react-native-community/cli-clean@12.3.0: resolution: {integrity: sha512-iAgLCOWYRGh9ukr+eVQnhkV/OqN3V2EGd/in33Ggn/Mj4uO6+oUncXFwB+yjlyaUNz6FfjudhIz09yYGSF+9sg==} dependencies: @@ -3838,6 +3856,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base-64@0.1.0: + resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}