diff --git a/app/(auth)/collections/[collection]/page.tsx b/app/(auth)/collections/[collection]/page.tsx deleted file mode 100644 index 504d9704..00000000 --- a/app/(auth)/collections/[collection]/page.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { Loading } from "@/components/Loading"; -import MoviePoster from "@/components/MoviePoster"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { Ionicons } from "@expo/vector-icons"; -import { - BaseItemDto, - ItemSortBy, -} from "@jellyfin/sdk/lib/generated-client/models"; -import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { router, useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai"; -import { useEffect, useMemo, useState } from "react"; -import { - ActivityIndicator, - ScrollView, - TouchableOpacity, - View, -} from "react-native"; - -const page: React.FC = () => { - const searchParams = useLocalSearchParams(); - const { collection: collectionId } = searchParams as { collection: string }; - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - useEffect(() => { - console.log("CollectionId", collectionId); - }, [collectionId]); - - const { data: collection } = useQuery({ - queryKey: ["collection", collectionId], - queryFn: async () => { - if (!api) return null; - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - ids: [collectionId], - }); - const data = response.data.Items?.[0]; - return data; - }, - enabled: !!api && !!user?.Id, - staleTime: 0, - }); - - const [startIndex, setStartIndex] = useState(0); - - const { data, isLoading, isError } = useQuery<{ - Items: BaseItemDto[]; - TotalRecordCount: number; - }>({ - queryKey: ["collection-items", collectionId, startIndex], - queryFn: async () => { - if (!api || !collectionId) - return { - Items: [], - TotalRecordCount: 0, - }; - - const sortBy: ItemSortBy[] = []; - - switch (collection?.CollectionType) { - case "movies": - sortBy.push("SortName", "ProductionYear"); - break; - case "boxsets": - sortBy.push("IsFolder", "SortName"); - break; - default: - sortBy.push("SortName"); - break; - } - - const response = await getItemsApi(api).getItems({ - userId: user?.Id, - parentId: collectionId, - limit: 100, - startIndex, - sortBy, - sortOrder: ["Ascending"], - }); - - const data = response.data.Items; - - return { - Items: data || [], - TotalRecordCount: response.data.TotalRecordCount || 0, - }; - }, - enabled: !!collectionId && !!api, - }); - // const { data, isLoading, isError } = useQuery<{ - // Items: BaseItemDto[]; - // TotalRecordCount: number; - // }>({ - // queryKey: ["collection-items", collectionId, startIndex], - // queryFn: async () => { - // if (!api) return []; - - // const response = await api.axiosInstance.get( - // `${api.basePath}/Users/${user?.Id}/Items`, - // { - // params: { - // SortBy: - // collection?.CollectionType === "movies" - // ? "SortName,ProductionYear" - // : "SortName", - // SortOrder: "Ascending", - // IncludeItemTypes: - // collection?.CollectionType === "movies" ? "Movie" : "Series", - // Recursive: true, - // Fields: - // collection?.CollectionType === "movies" - // ? "PrimaryImageAspectRatio,MediaSourceCount" - // : "PrimaryImageAspectRatio", - // ImageTypeLimit: 1, - // EnableImageTypes: "Primary,Backdrop,Banner,Thumb", - // ParentId: collectionId, - // Limit: 100, - // StartIndex: startIndex, - // }, - // headers: { - // Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, - // }, - // }, - // ); - - // return response.data || []; - // }, - // enabled: !!collection && !!api, - // }); - - const totalItems = useMemo(() => { - return data?.TotalRecordCount; - }, [data]); - - return ( - - - - {collection?.Name} - - - {startIndex + 1}-{Math.min(startIndex + 100, totalItems || 0)} of{" "} - {totalItems} - - - { - setStartIndex((prev) => Math.max(prev - 100, 0)); - }} - > - - - { - setStartIndex((prev) => prev + 100); - }} - > - - - - - - {isLoading ? ( - - - - ) : ( - - {data?.Items?.map((item: BaseItemDto, index: number) => ( - { - if (item?.Type === "Series") { - router.push(`/series/${item.Id}/page`); - } else if (item.IsFolder) { - router.push(`/collections/${item?.Id}/page`); - } else { - router.push(`/items/${item.Id}/page`); - } - }} - > - - - {item.Name} - - {item.ProductionYear} - - - - ))} - - )} - - {!isLoading && ( - - { - setStartIndex((prev) => Math.max(prev - 100, 0)); - }} - > - - - { - setStartIndex((prev) => prev + 100); - }} - > - - - - )} - - ); -}; - -export default page; diff --git a/app/(auth)/items/[id]/page.tsx b/app/(auth)/items/[id]/page.tsx deleted file mode 100644 index a9bcea02..00000000 --- a/app/(auth)/items/[id]/page.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { DownloadItem } from "@/components/DownloadItem"; -import { PlayedStatus } from "@/components/PlayedStatus"; -import { CastAndCrew } from "@/components/series/CastAndCrew"; -import { CurrentSeries } from "@/components/series/CurrentSeries"; -import { SimilarItems } from "@/components/SimilarItems"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useQuery } from "@tanstack/react-query"; -import { Image } from "expo-image"; -import { useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai"; -import { useCallback, useMemo, useState } from "react"; -import { ActivityIndicator, ScrollView, View } from "react-native"; -import { ParallaxScrollView } from "../../../../components/ParallaxPage"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { PlayButton } from "@/components/PlayButton"; -import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; -import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import CastContext, { - PlayServicesState, - useCastDevice, - useRemoteMediaClient, -} from "react-native-google-cast"; -import { chromecastProfile } from "@/utils/profiles/chromecast"; -import ios12 from "@/utils/profiles/ios12"; -import { - currentlyPlayingItemAtom, - triggerPlayAtom, -} from "@/components/CurrentlyPlayingBar"; -import { AudioTrackSelector } from "@/components/AudioTrackSelector"; -import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; -import { NextEpisodeButton } from "@/components/series/NextEpisodeButton"; -import { Ratings } from "@/components/Ratings"; -import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader"; -import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader"; -import { OverviewText } from "@/components/OverviewText"; - -const page: React.FC = () => { - const local = useLocalSearchParams(); - const { id } = local as { id: string }; - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const castDevice = useCastDevice(); - - const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]); - const [selectedAudioStream, setSelectedAudioStream] = useState(-1); - const [selectedSubtitleStream, setSelectedSubtitleStream] = - useState(0); - const [maxBitrate, setMaxBitrate] = useState({ - key: "Max", - value: undefined, - }); - - const { data: item, isLoading: l1 } = useQuery({ - queryKey: ["item", id], - queryFn: async () => - await getUserItemData({ - api, - userId: user?.Id, - itemId: id, - }), - enabled: !!id && !!api, - staleTime: 60, - }); - - const backdropUrl = useMemo( - () => - getBackdropUrl({ - api, - item, - quality: 90, - width: 1000, - }), - [item], - ); - - const logoUrl = useMemo( - () => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null), - [item], - ); - - const { data: sessionData } = useQuery({ - queryKey: ["sessionData", item?.Id], - queryFn: async () => { - if (!api || !user?.Id || !item?.Id) return null; - const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({ - itemId: item?.Id, - userId: user?.Id, - }); - - return playbackData.data; - }, - enabled: !!item?.Id && !!api && !!user?.Id, - staleTime: 0, - }); - - const { data: playbackUrl } = useQuery({ - queryKey: [ - "playbackUrl", - item?.Id, - maxBitrate, - castDevice, - selectedAudioStream, - selectedSubtitleStream, - ], - queryFn: async () => { - if (!api || !user?.Id || !sessionData) return null; - - const url = await getStreamUrl({ - api, - userId: user.Id, - item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, - maxStreamingBitrate: maxBitrate.value, - sessionData, - deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12, - audioStreamIndex: selectedAudioStream, - subtitleStreamIndex: selectedSubtitleStream, - }); - - console.log("Transcode URL: ", url); - - return url; - }, - enabled: !!sessionData, - staleTime: 0, - }); - - const [, setCp] = useAtom(currentlyPlayingItemAtom); - const client = useRemoteMediaClient(); - const [, setPlayTrigger] = useAtom(triggerPlayAtom); - - const onPressPlay = useCallback( - async (type: "device" | "cast" = "device") => { - if (!playbackUrl || !item) return; - - if (type === "cast" && client) { - await CastContext.getPlayServicesState().then((state) => { - if (state && state !== PlayServicesState.SUCCESS) - CastContext.showPlayServicesErrorDialog(state); - else { - client.loadMedia({ - mediaInfo: { - contentUrl: playbackUrl, - contentType: "video/mp4", - metadata: { - type: item.Type === "Episode" ? "tvShow" : "movie", - title: item.Name || "", - subtitle: item.Overview || "", - }, - }, - startTime: 0, - }); - } - }); - } else { - setCp({ - item, - playbackUrl, - }); - - // Use this trigger to initiate playback in another component (CurrentlyPlayingBar) - setPlayTrigger((prev) => prev + 1); - } - }, - [playbackUrl, item], - ); - - if (l1) - return ( - - - - ); - - if (!item?.Id || !backdropUrl) return null; - - return ( - - } - logo={ - <> - {logoUrl ? ( - - ) : null} - - } - > - - - {item.Type === "Episode" ? ( - - ) : ( - <> - - - )} - {item?.ProductionYear} - - - - - {playbackUrl ? ( - - ) : ( - - )} - - - - - - - - setMaxBitrate(val)} - selected={maxBitrate} - /> - - - - - - - - - - - - - Video - Audio - Subtitles - - - - {item.MediaStreams?.find((i) => i.Type === "Video")?.DisplayTitle} - - - {item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle} - - - { - item.MediaStreams?.find((i) => i.Type === "Subtitle") - ?.DisplayTitle - } - - - - - - - - {item.Type === "Episode" && ( - - - - )} - - - - - - ); -}; - -export default page; diff --git a/app/(auth)/player/offline/page.tsx b/app/(auth)/player/offline/page.tsx deleted file mode 100644 index 9e6de504..00000000 --- a/app/(auth)/player/offline/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { OfflineVideoPlayer } from "@/components/OfflineVideoPlayer"; -import * as FileSystem from "expo-file-system"; -import { useLocalSearchParams } from "expo-router"; -import { useMemo } from "react"; -import { View } from "react-native"; - -export default function page() { - const searchParams = useLocalSearchParams(); - const { itemId, url } = searchParams as { itemId: string; url: string }; - - const fileUrl = useMemo(() => { - const u = FileSystem.documentDirectory + url; - return u; - }, [url]); - - if (!fileUrl) return null; - - return ( - - {url && } - - ); -} diff --git a/app/(auth)/series/[id]/page.tsx b/app/(auth)/series/[id]/page.tsx deleted file mode 100644 index fcdfcb26..00000000 --- a/app/(auth)/series/[id]/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { ParallaxScrollView } from "@/components/ParallaxPage"; -import { NextUp } from "@/components/series/NextUp"; -import { SeasonPicker } from "@/components/series/SeasonPicker"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { useQuery } from "@tanstack/react-query"; -import { Image } from "expo-image"; -import { useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai"; -import { useEffect, useMemo } from "react"; -import { View } from "react-native"; - -const page: React.FC = () => { - const params = useLocalSearchParams(); - const { id: seriesId, seasonIndex } = params as { - id: string; - seasonIndex: string; - }; - - useEffect(() => { - if (seriesId) { - console.log("seasonIndex", seasonIndex); - } - }, [seriesId]); - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const { data: item } = useQuery({ - queryKey: ["series", seriesId], - queryFn: async () => - await getUserItemData({ - api, - userId: user?.Id, - itemId: seriesId, - }), - enabled: !!seriesId && !!api, - staleTime: 60, - }); - - const backdropUrl = useMemo( - () => - getBackdropUrl({ - api, - item, - quality: 90, - width: 1000, - }), - [item], - ); - - const logoUrl = useMemo( - () => - getLogoImageUrlById({ - api, - item, - }), - [item], - ); - - if (!item || !backdropUrl) return null; - - return ( - - } - logo={ - <> - {logoUrl ? ( - - ) : null} - - } - > - - - {item?.Name} - {item?.Overview} - - - - - - - - ); -}; - -export default page; diff --git a/components/NewVideoPlayer.tsx b/components/NewVideoPlayer.tsx deleted file mode 100644 index 13f002a4..00000000 --- a/components/NewVideoPlayer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useVideoPlayer, VideoView } from "expo-video"; -import { useEffect, useRef, useState } from "react"; -import { - PixelRatio, - StyleSheet, - View, - Button, - TouchableOpacity, -} from "react-native"; - -const videoSource = - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; - -interface Props { - videoSource: string; -} - -export const NewVideoPlayer: React.FC = ({ videoSource }) => { - const ref = useRef(null); - const [isPlaying, setIsPlaying] = useState(true); - const player = useVideoPlayer(videoSource, (player) => { - player.loop = true; - player.play(); - }); - - useEffect(() => { - const subscription = player.addListener("playingChange", (isPlaying) => { - setIsPlaying(isPlaying); - }); - - return () => { - subscription.remove(); - }; - }, [player]); - - return ( - { - ref.current?.enterFullscreen(); - }} - className={`relative h-full bg-neutral-800 rounded-md overflow-hidden - `} - > - - - ); -}; diff --git a/components/OldVideoPlayer.tsx b/components/OldVideoPlayer.tsx deleted file mode 100644 index 905a9ae9..00000000 --- a/components/OldVideoPlayer.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { runtimeTicksToMinutes } from "@/utils/time"; -import { Feather, Ionicons } from "@expo/vector-icons"; -import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useAtom } from "jotai"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { TouchableOpacity, View } from "react-native"; -import { useCastDevice, useRemoteMediaClient } from "react-native-google-cast"; -import Video, { OnProgressData, VideoRef } from "react-native-video"; -import * as DropdownMenu from "zeego/dropdown-menu"; -import { Button } from "./Button"; -import { Text } from "./common/Text"; -import ios12 from "../utils/profiles/ios12"; -import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress"; -import { chromecastProfile } from "@/utils/profiles/chromecast"; -import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import { currentlyPlayingItemAtom } from "./CurrentlyPlayingBar"; - -type VideoPlayerProps = { - itemId: string; - onChangePlaybackURL: (url: string | null) => void; -}; - -export const OldVideoPlayer: React.FC = ({ - itemId, - onChangePlaybackURL, -}) => { - const videoRef = useRef(null); - const [maxBitrate, setMaxbitrate] = useState(undefined); - const [paused, setPaused] = useState(true); - const [progress, setProgress] = useState(0); - - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const castDevice = useCastDevice(); - const client = useRemoteMediaClient(); - const queryClient = useQueryClient(); - - const { data: item } = useQuery({ - queryKey: ["item", itemId], - queryFn: async () => - await getUserItemData({ - api, - userId: user?.Id, - itemId, - }), - enabled: !!itemId && !!api, - staleTime: 60, - }); - - const { data: sessionData } = useQuery({ - queryKey: ["sessionData", itemId], - queryFn: async () => { - const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({ - itemId, - userId: user?.Id, - }); - - return playbackData.data; - }, - enabled: !!itemId && !!api && !!user?.Id, - staleTime: 0, - }); - - const { data: playbackURL } = useQuery({ - queryKey: ["playbackUrl", itemId, maxBitrate, castDevice], - queryFn: async () => { - if (!api || !user?.Id || !sessionData) return null; - - const url = await getStreamUrl({ - api, - userId: user.Id, - item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, - maxStreamingBitrate: maxBitrate, - sessionData, - deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12, - }); - - onChangePlaybackURL(url); - - return url; - }, - enabled: !!sessionData, - staleTime: 0, - }); - - const onProgress = useCallback( - ({ currentTime }: OnProgressData) => { - if (!currentTime || !sessionData?.PlaySessionId || paused) return; - const newProgress = currentTime * 10000000; - setProgress(newProgress); - reportPlaybackProgress({ - api, - itemId, - positionTicks: newProgress, - sessionId: sessionData.PlaySessionId, - }); - }, - [sessionData?.PlaySessionId, item, api, paused], - ); - - const play = () => { - if (videoRef.current) { - videoRef.current.resume(); - setPaused(false); - } - }; - - const pause = useCallback(() => { - videoRef.current?.pause(); - setPaused(true); - - if (progress > 0) - reportPlaybackStopped({ - api, - itemId: item?.Id, - positionTicks: progress, - sessionId: sessionData?.PlaySessionId, - }); - - queryClient.invalidateQueries({ - queryKey: ["nextUp", item?.SeriesId], - refetchType: "all", - }); - queryClient.invalidateQueries({ - queryKey: ["episodes"], - refetchType: "all", - }); - }, [api, item, progress, sessionData, queryClient]); - - const startPosition = useMemo( - () => - item?.UserData?.PlaybackPositionTicks - ? Math.round(item.UserData.PlaybackPositionTicks / 10000) - : 0, - [item], - ); - - const enableVideo = useMemo( - () => !!(playbackURL && item && startPosition !== null && sessionData), - [playbackURL, item, startPosition, sessionData], - ); - - const chromecastReady = useMemo( - () => !!(castDevice?.deviceId && item), - [castDevice, item], - ); - - const cast = useCallback(() => { - if (!client || !playbackURL || !item) return; - client.loadMedia({ - mediaInfo: { - contentUrl: playbackURL, - contentType: "video/mp4", - metadata: { - type: item.Type === "Episode" ? "tvShow" : "movie", - title: item.Name || "", - subtitle: item.Overview || "", - }, - streamDuration: Math.floor((item.RunTimeTicks || 0) / 10000), - }, - startTime: Math.floor( - (item.UserData?.PlaybackPositionTicks || 0) / 10000, - ), - }); - }, [item, client, playbackURL]); - - const [cp, setCp] = useAtom(currentlyPlayingItemAtom); - - useEffect(() => { - videoRef.current?.pause(); - }, []); - - return ( - - {enableVideo === true && startPosition !== null && !!playbackURL ? ( - - ); -};