diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b8717fcc..cb622e3b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,5 +1,5 @@ import { FetchError } from "ofetch"; -import { makeUrl, mwFetch } from "../helpers/fetch"; +import { makeUrl, proxiedFetch } from "../helpers/fetch"; import { formatJWMeta, JWMediaResult, @@ -45,7 +45,7 @@ export async function getMetaFromId( type: queryType, id, }); - data = await mwFetch(url, { baseURL: JW_API_BASE }); + data = await proxiedFetch(url, { baseURL: JW_API_BASE }); } catch (err) { if (err instanceof FetchError) { // 400 and 404 are treated as not found @@ -69,7 +69,7 @@ export async function getMetaFromId( const url = makeUrl("/content/titles/show_season/{id}/locale/en_US", { id: seasonToScrape, }); - seasonData = await mwFetch(url, { baseURL: JW_API_BASE }); + seasonData = await proxiedFetch(url, { baseURL: JW_API_BASE }); } return { diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 4ad0434b..1c3c4598 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,5 +1,5 @@ import { SimpleCache } from "@/utils/cache"; -import { mwFetch } from "../helpers/fetch"; +import { proxiedFetch } from "../helpers/fetch"; import { formatJWMeta, JWContentTypes, @@ -42,7 +42,7 @@ export async function searchForMedia(query: MWQuery): Promise { page_size: 40, }; - const data = await mwFetch>( + const data = await proxiedFetch>( "/content/titles/en_US/popular", { baseURL: JW_API_BASE, diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 2b8fa81e..54287d0c 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -25,6 +25,7 @@ export enum Icons { VOLUME_X = "volume_x", X = "x", EDIT = "edit", + AIRPLAY = "airplay", } export interface IconProps { @@ -57,6 +58,7 @@ const iconList: Record = { x: ``, edit: ``, bookmark_outline: ``, + airplay: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 11b4fd68..41d2d224 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,6 +1,7 @@ import { MWMediaMeta } from "@/backend/metadata/types"; import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; +import { AirplayControl } from "./controls/AirplayControl"; import { BackdropControl } from "./controls/BackdropControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; @@ -91,6 +92,7 @@ export function DecoratedVideoPlayer(
+
diff --git a/src/components/video/controls/AirplayControl.tsx b/src/components/video/controls/AirplayControl.tsx new file mode 100644 index 00000000..55ba7ec4 --- /dev/null +++ b/src/components/video/controls/AirplayControl.tsx @@ -0,0 +1,26 @@ +import { Icons } from "@/components/Icon"; +import { useCallback } from "react"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; +import { useVideoPlayerState } from "../VideoContext"; + +interface Props { + className?: string; +} + +export function AirplayControl(props: Props) { + const { videoState } = useVideoPlayerState(); + + const handleClick = useCallback(() => { + videoState.startAirplay(); + }, [videoState]); + + if (!videoState.canAirplay) return null; + + return ( + + ); +} diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 9ff7f5e8..531e94c3 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -30,6 +30,7 @@ export interface PlayerControls { setLeftControlsHover(hovering: boolean): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void; setShowData(data: ShowData): void; + startAirplay(): void; } export const initialControls: PlayerControls = { @@ -43,6 +44,7 @@ export const initialControls: PlayerControls = { setLeftControlsHover: () => null, initPlayer: () => null, setShowData: () => null, + startAirplay: () => null, }; export function populateControls( @@ -118,6 +120,11 @@ export function populateControls( setShowData(data) { update((s) => ({ ...s, seasonData: data })); }, + startAirplay() { + const videoPlayer = player as any; + if (videoPlayer.webkitShowPlaybackTargetPicker) + videoPlayer.webkitShowPlaybackTargetPicker(); + }, initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 296cd683..dfb929c7 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -34,6 +34,7 @@ export type PlayerState = { name: string; description: string; }; + canAirplay: boolean; }; export type PlayerContext = PlayerState & PlayerControls; @@ -57,6 +58,7 @@ export const initialPlayerState: PlayerContext = { seasonData: { isSeries: false, }, + canAirplay: false, ...initialControls, }; @@ -159,6 +161,14 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { : null, })); }; + const canAirplay = (e: any) => { + if (e.availability === "available") { + update((s) => ({ + ...s, + canAirplay: true, + })); + } + }; player.addEventListener("pause", pause); player.addEventListener("playing", playing); @@ -172,6 +182,10 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("waiting", waiting); player.addEventListener("canplay", canplay); player.addEventListener("error", error); + player.addEventListener( + "webkitplaybacktargetavailabilitychanged", + canAirplay + ); return () => { player.removeEventListener("pause", pause); @@ -186,6 +200,10 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("waiting", waiting); player.removeEventListener("canplay", canplay); player.removeEventListener("error", error); + player.removeEventListener( + "webkitplaybacktargetavailabilitychanged", + canAirplay + ); }; } diff --git a/src/index.tsx b/src/index.tsx index ea6781b3..5a471b6f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ if (key) { // - source selection // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) +// - safari progress bar cannot be dragged // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop @@ -41,7 +42,8 @@ if (key) { // - AFTER all that: rank providers/embedscrapers // TODO general todos: -// - localize everything +// - localize everything (fix loading screen text (series vs movies)) +// - make mobile friendly ReactDOM.render( diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 5265728f..d8014674 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -155,7 +155,6 @@ export function MediaView() { const [stream, setStream] = useState(null); useEffect(() => { - console.log("I am being ran"); exec(params.media, params.season).then((v) => { setMeta(v ?? null); if (v) {