diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e9ae48e7..8c9ad540 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -22,11 +22,6 @@
-
-
-
-
-
diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png b/android/app/src/main/res/drawable-hdpi/splashscreen_image.png
index 51785bdc..7ea0d77f 100644
Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-hdpi/splashscreen_image.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png b/android/app/src/main/res/drawable-mdpi/splashscreen_image.png
index 51785bdc..7ea0d77f 100644
Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-mdpi/splashscreen_image.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png
index 51785bdc..7ea0d77f 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png
index 51785bdc..7ea0d77f 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png
index 51785bdc..7ea0d77f 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 6aad50d8..5c2a7520 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index 6aad50d8..5c2a7520 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index df5e1733..277338c9 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 1317c233..b2a7de79 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index 1317c233..b2a7de79 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index f821ab19..5a7cf5a9 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 19a5881f..4a2f9c28 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index 19a5881f..4a2f9c28 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 5c7e700b..bde5a678 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index e4b5fc89..b4fa982a 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index e4b5fc89..b4fa982a 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index df159d9c..0ad5b061 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 69e01bbc..37e5c48e 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 69e01bbc..37e5c48e 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 336d67bb..db4280de 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app.json b/app.json
index e4ce296b..d99db8a3 100644
--- a/app.json
+++ b/app.json
@@ -4,11 +4,11 @@
"slug": "streamyfin",
"version": "0.0.4",
"orientation": "portrait",
- "icon": "./assets/images/icon.jpg",
+ "icon": "./assets/images/icon.png",
"scheme": "streamyfin",
"userInterfaceStyle": "dark",
"splash": {
- "image": "./assets/images/splash.jpg",
+ "image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
@@ -16,7 +16,8 @@
"ios": {
"userInterfaceStyle": "dark",
"infoPlist": {
- "NSCameraUsageDescription": "The app needs access to your camera to scan barcodes."
+ "NSCameraUsageDescription": "The app needs access to your camera to scan barcodes.",
+ "NSMicrophoneUsageDescription": "The app needs access to your microphone."
},
"supportsTablet": true,
"bundleIdentifier": "com.fredrikburmester.streamyfin"
@@ -24,22 +25,16 @@
"android": {
"userInterfaceStyle": "light",
"adaptiveIcon": {
- "foregroundImage": "./assets/images/icon.jpg",
+ "foregroundImage": "./assets/images/icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.fredrikburmester.streamyfin"
},
- "web": {
- "bundler": "metro",
- "output": "static",
- "favicon": "./assets/images/favicon.png"
- },
"plugins": [
"expo-router",
"expo-font",
"expo-video",
"react-native-compressor",
- // "react-native-google-cast",
[
"react-native-video",
{
diff --git a/app/(auth)/(tabs)/index.tsx b/app/(auth)/(tabs)/index.tsx
index b19d9995..994b783f 100644
--- a/app/(auth)/(tabs)/index.tsx
+++ b/app/(auth)/(tabs)/index.tsx
@@ -5,11 +5,13 @@ import { ItemCardText } from "@/components/ItemCardText";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
-import { useQuery } from "@tanstack/react-query";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
+import { useCallback, useState } from "react";
import {
ActivityIndicator,
+ RefreshControl,
ScrollView,
TouchableOpacity,
View,
@@ -21,7 +23,7 @@ export default function index() {
const router = useRouter();
const { data, isLoading, isError } = useQuery({
- queryKey: ["resumeItems", api, user?.Id],
+ queryKey: ["resumeItems", user?.Id],
queryFn: async () => {
if (!api || !user?.Id) {
return [];
@@ -85,6 +87,18 @@ export default function index() {
staleTime: 60,
});
+ const queryClient = useQueryClient();
+
+ const [loading, setLoading] = useState(false);
+
+ const refetch = useCallback(async () => {
+ setLoading(true);
+ await queryClient.refetchQueries({ queryKey: ["resumeItems", user?.Id] });
+ await queryClient.refetchQueries({ queryKey: ["items", user?.Id] });
+ await queryClient.refetchQueries({ queryKey: ["suggestions", user?.Id] });
+ setLoading(false);
+ }, [queryClient, user?.Id]);
+
if (isError)
return (
@@ -105,7 +119,12 @@ export default function index() {
if (!data || data.length === 0) return No data...;
return (
-
+
+ }
+ >
Continue Watching
diff --git a/app/(auth)/(tabs)/search.tsx b/app/(auth)/(tabs)/search.tsx
index 62410f29..d750ffb5 100644
--- a/app/(auth)/(tabs)/search.tsx
+++ b/app/(auth)/(tabs)/search.tsx
@@ -67,8 +67,8 @@ export default function search() {
return (
-
-
+
+
- Movies
+ Movies
m.Id!)}
renderItem={(data) => (
@@ -101,7 +101,7 @@ export default function search() {
/>
)}
/>
- Series
+ Series
m.Id!)}
renderItem={(data) => (
@@ -123,7 +123,7 @@ export default function search() {
/>
)}
/>
- Episodes
+ Episodes
m.Id!)}
renderItem={(data) => (
@@ -195,7 +195,7 @@ const SearchItemWrapper: React.FC = ({ ids, renderItem }) => {
staleTime: Infinity,
});
- if (!data) return No results;
+ if (!data) return No results;
return renderItem(data);
};
diff --git a/app/(auth)/items/[id]/ParallaxPage.tsx b/app/(auth)/items/[id]/ParallaxPage.tsx
new file mode 100644
index 00000000..a238e14f
--- /dev/null
+++ b/app/(auth)/items/[id]/ParallaxPage.tsx
@@ -0,0 +1,83 @@
+import type { PropsWithChildren, ReactElement } from "react";
+import {
+ NativeScrollEvent,
+ NativeSyntheticEvent,
+ StyleSheet,
+ useColorScheme,
+} from "react-native";
+import Animated, {
+ interpolate,
+ useAnimatedRef,
+ useAnimatedStyle,
+ useScrollViewOffset,
+} from "react-native-reanimated";
+
+import { ThemedView } from "@/components/ThemedView";
+
+const HEADER_HEIGHT = 250;
+
+type Props = PropsWithChildren<{
+ headerImage: ReactElement;
+ onScroll?: (event: NativeSyntheticEvent) => void;
+}>;
+
+export const ParallaxScrollView: React.FC = ({
+ children,
+ headerImage,
+ onScroll,
+}: Props) => {
+ const scrollRef = useAnimatedRef();
+ const scrollOffset = useScrollViewOffset(scrollRef);
+
+ const headerAnimatedStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ {
+ translateY: interpolate(
+ scrollOffset.value,
+ [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
+ [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
+ ),
+ },
+ {
+ scale: interpolate(
+ scrollOffset.value,
+ [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
+ [2, 1, 1]
+ ),
+ },
+ ],
+ };
+ });
+
+ return (
+
+
+
+ {headerImage}
+
+ {children}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ height: 250,
+ overflow: "hidden",
+ },
+ content: {
+ flex: 1,
+ overflow: "hidden",
+ },
+});
diff --git a/app/(auth)/items/[id]/page.tsx b/app/(auth)/items/[id]/page.tsx
index 91335eea..dc6c831e 100644
--- a/app/(auth)/items/[id]/page.tsx
+++ b/app/(auth)/items/[id]/page.tsx
@@ -13,8 +13,16 @@ import {} from "@jellyfin/sdk/lib/utils/url";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
-import { useEffect } from "react";
-import { ActivityIndicator, ScrollView, View } from "react-native";
+import { useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ NativeScrollEvent,
+ NativeSyntheticEvent,
+ ScrollView,
+ View,
+} from "react-native";
+import { ParallaxScrollView } from "./ParallaxPage";
+import { Image } from "expo-image";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -23,8 +31,6 @@ const page: React.FC = () => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
- const navigation = useNavigation();
-
const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", id],
queryFn: async () =>
@@ -34,22 +40,14 @@ const page: React.FC = () => {
itemId: id,
}),
enabled: !!id && !!api,
- staleTime: Infinity,
+ staleTime: 60,
});
- useEffect(() => {
- navigation.setOptions({
- headerRight: () => {
- ;
- },
- });
- }, [item, navigation]);
-
const { data: posterUrl } = useQuery({
queryKey: ["backdrop", item?.Id],
queryFn: async () => getBackdrop(api, item),
enabled: !!api && !!item?.Id,
- staleTime: Infinity,
+ staleTime: 60 * 60 * 24 * 7,
});
if (l1)
@@ -59,12 +57,23 @@ const page: React.FC = () => {
);
- if (!item?.Id) return null;
+ if (!item?.Id || !posterUrl) return null;
return (
-
-
-
+
+ }
+ >
+
{item.Type === "Episode" ? (
<>
@@ -127,12 +136,10 @@ const page: React.FC = () => {
-
-
-
+
{item.Type === "Episode" && (
-
+
)}
@@ -140,7 +147,7 @@ const page: React.FC = () => {
-
+
);
};
diff --git a/app/(auth)/series/[id]/page.tsx b/app/(auth)/series/[id]/page.tsx
index 86e768f1..f9862530 100644
--- a/app/(auth)/series/[id]/page.tsx
+++ b/app/(auth)/series/[id]/page.tsx
@@ -18,7 +18,7 @@ const page: React.FC = () => {
const [user] = useAtom(userAtom);
const { data: item } = useQuery({
- queryKey: ["item", seriesId],
+ queryKey: ["series", seriesId],
queryFn: async () =>
await getUserItemData({
api,
@@ -26,33 +26,23 @@ const page: React.FC = () => {
itemId: seriesId,
}),
enabled: !!seriesId && !!api,
- staleTime: Infinity,
- });
-
- const { data: next } = useQuery({
- queryKey: ["nextUp", seriesId],
- queryFn: async () =>
- await nextUp({
- userId: user?.Id,
- api,
- itemId: seriesId,
- }),
- enabled: !!api && !!seriesId && !!user?.Id,
- staleTime: 0,
+ staleTime: 60,
});
if (!item) return null;
return (
-
-
-
- {item?.Name}
- {item?.Overview}
+
+
+
+
+ {item?.Name}
+ {item?.Overview}
+
-
+
);
diff --git a/app/(auth)/settings.tsx b/app/(auth)/settings.tsx
index 6db4dc87..3d28f5d1 100644
--- a/app/(auth)/settings.tsx
+++ b/app/(auth)/settings.tsx
@@ -34,15 +34,22 @@ const deleteAllFiles = async () => {
const deleteFile = async (id: string | null | undefined) => {
if (!id) return;
- FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
- (err) => console.error(err)
- );
+ try {
+ FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
+ (err) => console.error(err)
+ );
- const currentFiles = JSON.parse(
- (await AsyncStorage.getItem("downloaded_files")) ?? "[]"
- );
- const updatedFiles = currentFiles.filter((f: string) => f !== id);
- await AsyncStorage.setItem("downloaded_files", JSON.stringify(updatedFiles));
+ const currentFiles = JSON.parse(
+ (await AsyncStorage.getItem("downloaded_files")) ?? "[]"
+ ) as BaseItemDto[];
+ const updatedFiles = currentFiles.filter((f) => f.Id !== id);
+ await AsyncStorage.setItem(
+ "downloaded_files",
+ JSON.stringify(updatedFiles)
+ );
+ } catch (error) {
+ console.error(error);
+ }
};
const listDownloadedFiles = async () => {
@@ -125,8 +132,8 @@ export default function settings() {
subTitle={file.ProductionYear?.toString()}
iconAfter={
{
- deleteFile(file.Id);
+ onPress={async () => {
+ await deleteFile(file.Id);
setKey((prevKey) => prevKey + 1);
}}
>
@@ -142,18 +149,20 @@ export default function settings() {
))}
) : activeProcess ? (
-
- }
- />
+
+
+ }
+ />
+
) : (
No downloaded files
)}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index c81384f9..e0564d46 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -30,12 +30,10 @@ export default function RootLayout() {
defaultOptions: {
queries: {
staleTime: 60,
- refetchOnMount: false,
- refetchInterval: false,
- refetchIntervalInBackground: false,
- refetchOnReconnect: false,
- refetchOnWindowFocus: false,
- retryOnMount: false,
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ refetchOnWindowFocus: true,
+ retryOnMount: true,
},
},
})
diff --git a/assets/images/icon.png b/assets/images/icon.png
new file mode 100644
index 00000000..01b93990
Binary files /dev/null and b/assets/images/icon.png differ
diff --git a/assets/images/splash.png b/assets/images/splash.png
new file mode 100644
index 00000000..ebb588a2
Binary files /dev/null and b/assets/images/splash.png differ
diff --git a/bun.lockb b/bun.lockb
index cd531206..7c43504a 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx
index 4fff599f..b7daa5e6 100644
--- a/components/ContinueWatchingPoster.tsx
+++ b/components/ContinueWatchingPoster.tsx
@@ -23,7 +23,7 @@ const ContinueWatchingPoster: React.FC = ({
queryKey: ["backdrop", item.Id],
queryFn: async () => getBackdrop(api, item),
enabled: !!api && !!item.Id,
- staleTime: Infinity,
+ staleTime: 60 * 60 * 24 * 7,
});
const [progress, setProgress] = useState(
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index 1de0d601..9640b2d4 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -1,182 +1,50 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
-import { writeToLog } from "@/utils/log";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { runningProcesses } from "@/utils/atoms/downloads";
+import { getPlaybackInfo, useDownloadMedia } from "@/utils/jellyfin";
import Ionicons from "@expo/vector-icons/Ionicons";
import AsyncStorage from "@react-native-async-storage/async-storage";
-import * as FileSystem from "expo-file-system";
-import { FFmpegKit, FFmpegKitConfig } from "ffmpeg-kit-react-native";
-import { atom, useAtom } from "jotai";
-import { useCallback, useEffect, useState } from "react";
-import { TouchableOpacity, View } from "react-native";
-import ProgressCircle from "./ProgressCircle";
import { router } from "expo-router";
-import { getPlaybackInfo, useDownloadMedia } from "@/utils/jellyfin";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { ProcessItem, runningProcesses } from "@/utils/atoms/downloads";
+import { useAtom } from "jotai";
+import { useCallback, useEffect, useState } from "react";
+import { ActivityIndicator, TouchableOpacity, View } from "react-native";
+import ProgressCircle from "./ProgressCircle";
+import { Text } from "./common/Text";
+import { useQuery } from "@tanstack/react-query";
type DownloadProps = {
item: BaseItemDto;
};
-// const useRemuxHlsToMp4 = (inputUrl: string, item: BaseItemDto) => {
-// if (!item.Id || !item.Name) {
-// writeToLog("ERROR", "useRemuxHlsToMp4 ~ missing arguments", {
-// item,
-// inputUrl,
-// });
-// throw new Error("Item must have an Id and Name");
-// }
-
-// const [session, setSession] = useAtom(runningProcesses);
-
-// const output = `${FileSystem.documentDirectory}${item.Id}.mp4`;
-
-// const command = `-y -fflags +genpts -i ${inputUrl} -c copy -max_muxing_queue_size 9999 ${output}`;
-
-// const startRemuxing = useCallback(async () => {
-// if (!item.Id || !item.Name) {
-// writeToLog(
-// "ERROR",
-// "useRemuxHlsToMp4 ~ startRemuxing ~ missing arguments",
-// {
-// item,
-// inputUrl,
-// }
-// );
-// throw new Error("Item must have an Id and Name");
-// }
-
-// writeToLog(
-// "INFO",
-// `useRemuxHlsToMp4 ~ startRemuxing for item ${item.Id} with url ${inputUrl}`,
-// {
-// item,
-// inputUrl,
-// }
-// );
-
-// try {
-// setSession({
-// item,
-// progress: 0,
-// });
-
-// FFmpegKitConfig.enableStatisticsCallback((statistics) => {
-// let percentage = 0;
-
-// const videoLength =
-// (item.MediaSources?.[0].RunTimeTicks || 0) / 10000000; // In seconds
-// const fps = item.MediaStreams?.[0].RealFrameRate || 25;
-// const totalFrames = videoLength * fps;
-
-// const processedFrames = statistics.getVideoFrameNumber();
-
-// if (totalFrames > 0) {
-// percentage = Math.floor((processedFrames / totalFrames) * 100);
-// }
-
-// setSession((prev) => {
-// return prev?.item.Id === item.Id!
-// ? { ...prev, progress: percentage }
-// : prev;
-// });
-// });
-
-// await FFmpegKit.executeAsync(command, async (session) => {
-// const returnCode = await session.getReturnCode();
-// if (returnCode.isValueSuccess()) {
-// const currentFiles: BaseItemDto[] = JSON.parse(
-// (await AsyncStorage.getItem("downloaded_files")) || "[]"
-// );
-
-// const otherItems = currentFiles.filter((i) => i.Id !== item.Id);
-
-// await AsyncStorage.setItem(
-// "downloaded_files",
-// JSON.stringify([...otherItems, item])
-// );
-
-// writeToLog(
-// "INFO",
-// `useRemuxHlsToMp4 ~ remuxing completed successfully for item: ${item.Name}`,
-// {
-// item,
-// inputUrl,
-// }
-// );
-// setSession(null);
-// } else if (returnCode.isValueError()) {
-// console.error("Failed to remux:");
-// writeToLog(
-// "ERROR",
-// `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}`,
-// {
-// item,
-// inputUrl,
-// }
-// );
-// setSession(null);
-// } else if (returnCode.isValueCancel()) {
-// console.log("Remuxing was cancelled");
-// writeToLog(
-// "INFO",
-// `useRemuxHlsToMp4 ~ remuxing was canceled for item: ${item.Name}`,
-// {
-// item,
-// inputUrl,
-// }
-// );
-// setSession(null);
-// }
-// });
-// } catch (error) {
-// console.error("Failed to remux:", error);
-// writeToLog(
-// "ERROR",
-// `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}`,
-// {
-// item,
-// inputUrl,
-// }
-// );
-// }
-// }, [inputUrl, output, item, command]);
-
-// const cancelRemuxing = useCallback(async () => {
-// FFmpegKit.cancel();
-// setSession(null);
-// console.log("Remuxing cancelled");
-// }, []);
-
-// return { session, startRemuxing, cancelRemuxing };
-// };
-
export const DownloadItem: React.FC = ({ item }) => {
- // const { session, startRemuxing, cancelRemuxing } = useRemuxHlsToMp4(
- // url,
- // item
- // );
-
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [process] = useAtom(runningProcesses);
- const { downloadMedia, isDownloading, error } = useDownloadMedia(api);
+ const { downloadMedia, isDownloading, error, cancelDownload } =
+ useDownloadMedia(api, user?.Id);
+
+ const { data: playbackInfo, isLoading } = useQuery({
+ queryKey: ["playbackInfo", item.Id],
+ queryFn: async () => getPlaybackInfo(api, item.Id, user?.Id),
+ });
const downloadFile = useCallback(async () => {
- const playbackInfo = await getPlaybackInfo(api, item.Id, user?.Id);
+ if (!playbackInfo) return;
- const source = playbackInfo?.MediaSources?.[0];
+ const source = playbackInfo.MediaSources?.[0];
if (source?.SupportsDirectPlay && item.CanDownload) {
downloadMedia(item);
} else {
- console.log("file not supported");
+ throw new Error(
+ "Direct play not supported thus the file cannot be downloaded"
+ );
}
- }, [item, user]);
+ }, [item, user, playbackInfo]);
const [downloaded, setDownloaded] = useState(false);
- const [key, setKey] = useState("");
useEffect(() => {
(async () => {
@@ -186,7 +54,19 @@ export const DownloadItem: React.FC = ({ item }) => {
if (data.find((d) => d.Id === item.Id)) setDownloaded(true);
})();
- }, [key]);
+ }, [process]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (playbackInfo?.MediaSources?.[0].SupportsDirectPlay === false) {
+ return (
+
+
+
+ );
+ }
if (process && process.item.Id !== item.Id!) {
return (
@@ -201,17 +81,22 @@ export const DownloadItem: React.FC = ({ item }) => {
{process ? (
{
- // cancelRemuxing();
+ cancelDownload();
}}
- className="-rotate-45"
+ className="relative"
>
-
+
+
+
+
+ {process.progress.toFixed(0)}%
+
) : downloaded ? (
= ({ item }) => {
);
}}
>
-
+
) : (
= ({ item }) => {
downloadFile();
}}
>
-
+
)}
diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx
index b527f1d0..d6b73442 100644
--- a/components/PlayedStatus.tsx
+++ b/components/PlayedStatus.tsx
@@ -4,7 +4,7 @@ import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient, InvalidateQueryFilters } from "@tanstack/react-query";
import { useAtom } from "jotai";
-import React from "react";
+import React, { useCallback } from "react";
import { TouchableOpacity, View } from "react-native";
import * as Haptics from "expo-haptics";
@@ -14,6 +14,29 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
const queryClient = useQueryClient();
+ const invalidateQueries = useCallback(() => {
+ queryClient.invalidateQueries({
+ queryKey: ["item", item.Id],
+ refetchType: "all",
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["resumeItems", user?.Id],
+ refetchType: "all",
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["nextUp", item.SeriesId],
+ refetchType: "all",
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["episodes"],
+ refetchType: "all",
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["seasons"],
+ refetchType: "all",
+ });
+ }, [api, item.Id, queryClient, user?.Id]);
+
return (
{item.UserData?.Played ? (
@@ -24,11 +47,8 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
itemId: item?.Id,
userId: user?.Id,
});
- queryClient.invalidateQueries({
- queryKey: ["item", item.Id],
- refetchType: "all",
- });
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ invalidateQueries();
}}
>
@@ -41,11 +61,8 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
itemId: item?.Id,
userId: user?.Id,
});
- queryClient.invalidateQueries({
- queryKey: ["item", item.Id],
- refetchType: "all",
- });
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ invalidateQueries();
}}
>
diff --git a/components/VideoPlayer.tsx b/components/VideoPlayer.tsx
index 660fe15e..67715a93 100644
--- a/components/VideoPlayer.tsx
+++ b/components/VideoPlayer.tsx
@@ -1,14 +1,17 @@
-import React, { useEffect, useMemo, useRef, useState } from "react";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
- ActivityIndicator,
- Switch,
- TouchableOpacity,
- View,
-} from "react-native";
-import * as DropdownMenu from "zeego/dropdown-menu";
+ getStreamUrl,
+ getUserItemData,
+ reportPlaybackProgress,
+ reportPlaybackStopped,
+} from "@/utils/jellyfin";
+import { runtimeTicksToMinutes } from "@/utils/time";
+import { Ionicons } from "@expo/vector-icons";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import Video, {
OnBufferData,
OnPlaybackStateChangedData,
@@ -16,17 +19,8 @@ import Video, {
OnVideoErrorData,
VideoRef,
} from "react-native-video";
-import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
-import {
- getBackdrop,
- getStreamUrl,
- getUserItemData,
- reportPlaybackProgress,
- reportPlaybackStopped,
-} from "@/utils/jellyfin";
-import { Ionicons } from "@expo/vector-icons";
+import * as DropdownMenu from "zeego/dropdown-menu";
import { Button } from "./Button";
-import { runtimeTicksToMinutes } from "@/utils/time";
import { Text } from "./common/Text";
type VideoPlayerProps = {
@@ -36,11 +30,7 @@ type VideoPlayerProps = {
const BITRATES = [
{
key: "Max",
- value: 140000000,
- },
- {
- key: "10 Mb/s",
- value: 10000000,
+ value: undefined,
},
{
key: "4 Mb/s",
@@ -50,10 +40,6 @@ const BITRATES = [
key: "2 Mb/s",
value: 2000000,
},
- {
- key: "1 Mb/s",
- value: 1000000,
- },
{
key: "500 Kb/s",
value: 500000,
@@ -62,12 +48,8 @@ const BITRATES = [
export const VideoPlayer: React.FC = ({ itemId }) => {
const videoRef = useRef(null);
- const [showPoster, setShowPoster] = useState(true);
- const [isPlaying, setIsPlaying] = useState(false);
- const [buffering, setBuffering] = useState(false);
- const [maxBitrate, setMaxbitrate] = useState(140000000);
+ const [maxBitrate, setMaxbitrate] = useState(undefined);
const [paused, setPaused] = useState(true);
- const [forceTranscoding, setForceTranscoding] = useState(false);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -81,7 +63,7 @@ export const VideoPlayer: React.FC = ({ itemId }) => {
itemId,
}),
enabled: !!itemId && !!api,
- staleTime: 0,
+ staleTime: 60,
});
const { data: sessionData } = useQuery({
@@ -182,7 +164,9 @@ export const VideoPlayer: React.FC = ({ itemId }) => {
return (
- {playbackURL && (
+ {enableVideo === true &&
+ playbackURL !== null &&
+ playbackURL !== undefined ? (