diff --git a/app.json b/app.json
index 438c220f..c8a31d8e 100644
--- a/app.json
+++ b/app.json
@@ -39,11 +39,7 @@
"expo-router",
"expo-font",
"react-native-compressor",
- [
- "react-native-google-cast",
- {
- }
- ],
+ ["react-native-google-cast", {}],
[
"react-native-video",
{
@@ -56,6 +52,17 @@
}
}
],
+ [
+ "react-native-vlc-media-player",
+ {
+ "ios": {
+ "includeVLCKit": false
+ },
+ "android": {
+ "legacyJetifier": false
+ }
+ }
+ ],
["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }]
],
"experiments": {
diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx
index 2b4e4232..10e828e5 100644
--- a/app/(auth)/(tabs)/_layout.tsx
+++ b/app/(auth)/(tabs)/_layout.tsx
@@ -27,6 +27,16 @@ export default function TabLayout() {
color={color}
/>
),
+ headerLeft: () => (
+ {
+ router.push("/(auth)/downloads");
+ }}
+ >
+
+
+ ),
headerRight: () => (
);
- if (!data || data.length === 0) return No data...;
-
return (
{
+ const { data: downloadedFiles } = useQuery({
+ queryKey: ["downloaded_files"],
+ queryFn: async () =>
+ JSON.parse(
+ (await AsyncStorage.getItem("downloaded_files")) || "[]"
+ ) as BaseItemDto[],
+ });
+
+ const movies = useMemo(
+ () => downloadedFiles?.filter((f) => f.Type === "Movie"),
+ [downloadedFiles]
+ );
+
+ const groupedBySeries = useMemo(() => {
+ const episodes = downloadedFiles?.filter((f) => f.Type === "Episode");
+ const series: { [key: string]: BaseItemDto[] } = {};
+ episodes?.forEach((e) => {
+ if (!series[e.SeriesName!]) series[e.SeriesName!] = [];
+ series[e.SeriesName!].push(e);
+ });
+ return Object.values(series);
+ }, [downloadedFiles]);
+
+ useEffect(() => {
+ console.log(
+ downloadedFiles?.map((i) => ({
+ name: i.Name,
+ codec: i.SourceType,
+ media: i.MediaSources?.[0].Container,
+ }))
+ );
+ }, [downloadedFiles]);
+
+ useEffect(() => {
+ // Get all files from FileStorage
+ // const filename = `${itemId}.mp4`;
+ // const fileUri = `${FileSystem.documentDirectory}`;
+ (async () => {
+ if (!FileSystem.documentDirectory) return;
+ const f = await FileSystem.readDirectoryAsync(
+ FileSystem.documentDirectory
+ );
+ console.log("files", FileSystem.documentDirectory, f);
+ })();
+ }, []);
+
+ return (
+
+
+
+
+ Movies
+
+ {movies?.length}
+
+
+ {movies?.map((item: BaseItemDto) => (
+
+
+
+ ))}
+
+
+ {groupedBySeries?.map((items: BaseItemDto[], index: number) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default downloads;
diff --git a/app/(auth)/items/[id]/page.tsx b/app/(auth)/items/[id]/page.tsx
index bb051ab9..61c49f62 100644
--- a/app/(auth)/items/[id]/page.tsx
+++ b/app/(auth)/items/[id]/page.tsx
@@ -1,3 +1,4 @@
+import { Chromecast } from "@/components/Chromecast";
import { Text } from "@/components/common/Text";
import { DownloadItem } from "@/components/DownloadItem";
import { PlayedStatus } from "@/components/PlayedStatus";
@@ -9,7 +10,6 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
getBackdrop,
getLogoImageById,
- getPrimaryImage,
getUserItemData,
} from "@/utils/jellyfin";
import { useQuery } from "@tanstack/react-query";
@@ -24,8 +24,6 @@ import {
View,
} from "react-native";
import { ParallaxScrollView } from "../../../../components/ParallaxPage";
-import { Chromecast } from "@/components/Chromecast";
-import { useRemoteMediaClient } from "react-native-google-cast";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -114,7 +112,7 @@ const page: React.FC = () => {
{item?.SeriesName}
-
+
{item?.Name}
diff --git a/app/(auth)/player/offline/page.tsx b/app/(auth)/player/offline/page.tsx
index 6780e9aa..6462972e 100644
--- a/app/(auth)/player/offline/page.tsx
+++ b/app/(auth)/player/offline/page.tsx
@@ -9,7 +9,9 @@ export default function page() {
const { itemId, url } = searchParams as { itemId: string; url: string };
const fileUrl = useMemo(() => {
- return FileSystem.documentDirectory + url;
+ const u = FileSystem.documentDirectory + url;
+ console.log({ u });
+ return u;
}, [url]);
if (!fileUrl) return null;
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 3f5c4ab8..670e3d87 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -77,6 +77,19 @@ export default function RootLayout() {
),
}}
/>
+ (
+ router.back()}>
+
+
+ ),
+ }}
+ />
= ({ itemId }) => {
staleTime: Infinity,
});
+ const movies = useMemo(
+ () => similarItems?.filter((i) => i.Type === "Movie") || [],
+ [similarItems]
+ );
+
return (
Similar items
@@ -48,22 +55,20 @@ export const SimilarItems: React.FC = ({ itemId }) => {
) : (
- {similarItems?.map((item) => (
+ {movies.map((item) => (
router.push(`/items/${item.Id}/page`)}
- className="flex flex-col w-48"
+ className="flex flex-col w-32"
>
-
+
))}
)}
- {similarItems?.length === 0 && (
- No similar items
- )}
+ {movies.length === 0 && No similar items}
);
};
diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx
new file mode 100644
index 00000000..69974a04
--- /dev/null
+++ b/components/downloads/EpisodeCard.tsx
@@ -0,0 +1,23 @@
+import { TouchableOpacity, View } from "react-native";
+import { Text } from "../common/Text";
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { router } from "expo-router";
+
+export const EpisodeCard: React.FC<{ item: BaseItemDto }> = ({ item }) => {
+ const open = () => {
+ router.back();
+ router.push(
+ `/(auth)/player/offline/page?url=${item.Id}.${item.MediaSources?.[0].Container}&itemId=${item.Id}`
+ );
+ };
+
+ return (
+
+ {item.Name}
+ Episode {item.IndexNumber}
+
+ );
+};
diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx
new file mode 100644
index 00000000..4f47bcc7
--- /dev/null
+++ b/components/downloads/MovieCard.tsx
@@ -0,0 +1,18 @@
+import { View } from "react-native";
+import { Text } from "../common/Text";
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { runtimeTicksToMinutes } from "@/utils/time";
+
+export const MovieCard: React.FC<{ item: BaseItemDto }> = ({ item }) => {
+ return (
+
+ {item.Name}
+
+ {item.ProductionYear}
+
+ {runtimeTicksToMinutes(item.RunTimeTicks)}
+
+
+
+ );
+};
diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx
new file mode 100644
index 00000000..b1470fe5
--- /dev/null
+++ b/components/downloads/SeriesCard.tsx
@@ -0,0 +1,49 @@
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { View } from "react-native";
+import { EpisodeCard } from "./EpisodeCard";
+import { Text } from "../common/Text";
+import { useMemo } from "react";
+import { SeasonPicker } from "../series/SeasonPicker";
+
+export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
+ const groupBySeason = useMemo(() => {
+ const seasons: Record = {};
+
+ items.forEach((item) => {
+ if (!seasons[item.SeasonName!]) {
+ seasons[item.SeasonName!] = [];
+ }
+
+ seasons[item.SeasonName!].push(item);
+ });
+
+ return Object.values(seasons).sort(
+ (a, b) => a[0].IndexNumber! - b[0].IndexNumber!
+ );
+ }, [items]);
+
+ return (
+
+
+ {items[0].SeriesName}
+
+ {items.length}
+
+
+
+ TV-Series
+ {groupBySeason.map((seasonItems, seasonIndex) => (
+
+
+ {seasonItems[0].SeasonName}
+
+ {seasonItems.map((item, index) => (
+
+
+
+ ))}
+
+ ))}
+
+ );
+};
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2f8c24f3..d10b5da2 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -329,6 +329,7 @@ PODS:
- MMKV (1.3.4):
- MMKVCore (~> 1.3.4)
- MMKVCore (1.3.9)
+ - MobileVLCKit (3.5.1)
- PromisesObjC (2.4.0)
- Protobuf (3.27.3)
- RCT-Folly (2024.01.01.00):
@@ -1291,6 +1292,8 @@ PODS:
- React
- react-native-menu (1.1.2):
- React
+ - react-native-netinfo (11.3.1):
+ - React-Core
- react-native-safe-area-context (4.10.5):
- React-Core
- react-native-video (6.4.3):
@@ -1336,6 +1339,10 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - react-native-vlc-media-player (1.0.38):
+ - MobileVLCKit (= 3.5.1)
+ - React
+ - TVVLCKit (= 3.5.1)
- React-nativeconfig (0.74.3)
- React-NativeModulesApple (0.74.3):
- glog
@@ -1717,8 +1724,10 @@ DEPENDENCIES:
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-google-cast (from `../node_modules/react-native-google-cast`)
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
+ - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-video (from `../node_modules/react-native-video`)
+ - react-native-vlc-media-player (from `../node_modules/react-native-vlc-media-player`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1763,6 +1772,7 @@ SPEC REPOS:
- libwebp
- MMKV
- MMKVCore
+ - MobileVLCKit
- PromisesObjC
- Protobuf
- SDWebImage
@@ -1889,10 +1899,14 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-google-cast"
react-native-menu:
:path: "../node_modules/@react-native-menu/menu"
+ react-native-netinfo:
+ :path: "../node_modules/@react-native-community/netinfo"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-video:
:path: "../node_modules/react-native-video"
+ react-native-vlc-media-player:
+ :path: "../node_modules/react-native-vlc-media-player"
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
@@ -1996,6 +2010,7 @@ SPEC CHECKSUMS:
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74
MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9
+ MobileVLCKit: 144d5f565512d1147d63b0fa1379231b3fd66535
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
Protobuf: c1cbc880ea7c4e9b157e113515c720a22ee7cf50
RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47
@@ -2027,8 +2042,10 @@ SPEC CHECKSUMS:
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-google-cast: 77c4a3acca9eab527328f8b65257d952be5f220f
react-native-menu: d32728a357dfb360cf01cd5979cf7713c5acbb95
+ react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321
react-native-safe-area-context: a240ad4b683349e48b1d51fed1611138d1bdad97
react-native-video: b3ba8f424c8c3f54dd9289d47bbe60fbc09bc986
+ react-native-vlc-media-player: 0f9860bded57cd6764ac55b5c0e97347069cb40a
React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb
React-NativeModulesApple: 585d1b78e0597de364d259cb56007052d0bda5e5
React-perflogger: 7bb9ba49435ff66b666e7966ee10082508a203e8
diff --git a/ios/Streamyfin.xcodeproj/project.pbxproj b/ios/Streamyfin.xcodeproj/project.pbxproj
index c391f8a7..3ae1193e 100644
--- a/ios/Streamyfin.xcodeproj/project.pbxproj
+++ b/ios/Streamyfin.xcodeproj/project.pbxproj
@@ -10,10 +10,10 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
+ 303D6C1B994E4AB730BB4D25 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 5677A7148111D7C0235E3185 /* PrivacyInfo.xcprivacy */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
- 76DE60737AC6B111D4CF6135 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6AFEBF12046598BC1A9FEC11 /* PrivacyInfo.xcprivacy */; };
+ 7B8C1837932545C6A7001A29 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46971613083A41BAB9656F14 /* noop-file.swift */; };
96905EF65AED1B983A6B3ABC /* libPods-Streamyfin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Streamyfin.a */; };
- 9B470C0ACF444752B7807218 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BFAF74683646218FA987CE /* noop-file.swift */; };
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
/* End PBXBuildFile section */
@@ -25,14 +25,14 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Streamyfin/Images.xcassets; sourceTree = ""; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Streamyfin/Info.plist; sourceTree = ""; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Streamyfin/main.m; sourceTree = ""; };
- 2092729FD1D54853BF6A8489 /* Streamyfin-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Streamyfin-Bridging-Header.h"; path = "Streamyfin/Streamyfin-Bridging-Header.h"; sourceTree = ""; };
+ 46971613083A41BAB9656F14 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Streamyfin/noop-file.swift"; sourceTree = ""; };
+ 5677A7148111D7C0235E3185 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Streamyfin/PrivacyInfo.xcprivacy; sourceTree = ""; };
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Streamyfin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Streamyfin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
- 6AFEBF12046598BC1A9FEC11 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Streamyfin/PrivacyInfo.xcprivacy; sourceTree = ""; };
6C2E3173556A471DD304B334 /* Pods-Streamyfin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Streamyfin.debug.xcconfig"; path = "Target Support Files/Pods-Streamyfin/Pods-Streamyfin.debug.xcconfig"; sourceTree = ""; };
7A4D352CD337FB3A3BF06240 /* Pods-Streamyfin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Streamyfin.release.xcconfig"; path = "Target Support Files/Pods-Streamyfin/Pods-Streamyfin.release.xcconfig"; sourceTree = ""; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Streamyfin/SplashScreen.storyboard; sourceTree = ""; };
- B3BFAF74683646218FA987CE /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Streamyfin/noop-file.swift"; sourceTree = ""; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; };
+ E9097609D0714599AF91F55B /* Streamyfin-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Streamyfin-Bridging-Header.h"; path = "Streamyfin/Streamyfin-Bridging-Header.h"; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Streamyfin/ExpoModulesProvider.swift"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -59,9 +59,9 @@
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB71A68108700A75B9A /* main.m */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
- B3BFAF74683646218FA987CE /* noop-file.swift */,
- 2092729FD1D54853BF6A8489 /* Streamyfin-Bridging-Header.h */,
- 6AFEBF12046598BC1A9FEC11 /* PrivacyInfo.xcprivacy */,
+ 46971613083A41BAB9656F14 /* noop-file.swift */,
+ E9097609D0714599AF91F55B /* Streamyfin-Bridging-Header.h */,
+ 5677A7148111D7C0235E3185 /* PrivacyInfo.xcprivacy */,
);
name = Streamyfin;
sourceTree = "";
@@ -147,13 +147,13 @@
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Streamyfin" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
- E4ADA958330463261FFFF98E /* [Expo] Configure project */,
+ 5AE4F51AB7DD565AD6F401F0 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
- FE3C21F6FE840DF7275CA666 /* [CP] Embed Pods Frameworks */,
+ E8388EA82119C85BBE90EA67 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -203,7 +203,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
- 76DE60737AC6B111D4CF6135 /* PrivacyInfo.xcprivacy in Resources */,
+ 303D6C1B994E4AB730BB4D25 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -247,6 +247,25 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
+ 5AE4F51AB7DD565AD6F401F0 /* [Expo] Configure project */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "[Expo] Configure project";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Streamyfin/expo-configure-project.sh\"\n";
+ };
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -295,32 +314,14 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- E4ADA958330463261FFFF98E /* [Expo] Configure project */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- name = "[Expo] Configure project";
- outputFileListPaths = (
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Streamyfin/expo-configure-project.sh\"\n";
- };
- FE3C21F6FE840DF7275CA666 /* [CP] Embed Pods Frameworks */ = {
+ E8388EA82119C85BBE90EA67 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-frameworks.sh",
+ "${PODS_XCFRAMEWORKS_BUILD_DIR}/MobileVLCKit/MobileVLCKit.framework/MobileVLCKit",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/ffmpegkit.framework/ffmpegkit",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavcodec.framework/libavcodec",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavdevice.framework/libavdevice",
@@ -333,6 +334,7 @@
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MobileVLCKit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ffmpegkit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavdevice.framework",
@@ -358,7 +360,7 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
- 9B470C0ACF444752B7807218 /* noop-file.swift in Sources */,
+ 7B8C1837932545C6A7001A29 /* noop-file.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ios/Streamyfin/AppDelegate.mm b/ios/Streamyfin/AppDelegate.mm
index 97ea8abc..aef5365a 100644
--- a/ios/Streamyfin/AppDelegate.mm
+++ b/ios/Streamyfin/AppDelegate.mm
@@ -22,7 +22,6 @@
options.suspendSessionsWhenBackgrounded = true;
[GCKCastContext setSharedInstanceWithOptions:options];
[GCKCastContext sharedInstance].useDefaultExpandedMediaControls = true;
-
#endif
// @generated end react-native-google-cast-didFinishLaunchingWithOptions
self.moduleName = @"main";
diff --git a/package.json b/package.json
index d1dffdb9..6d0e5577 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@jellyfin/sdk": "^0.10.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.0",
"@react-native-async-storage/async-storage": "1.23.1",
+ "@react-native-community/netinfo": "11.3.1",
"@react-native-menu/menu": "^1.1.2",
"@react-navigation/native": "^6.0.2",
"@tanstack/react-query": "^5.51.16",
@@ -57,6 +58,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2",
"react-native-video": "^6.4.3",
+ "react-native-vlc-media-player": "^1.0.67",
"react-native-web": "~0.19.10",
"tailwindcss": "3.3.2",
"uuid": "^10.0.0",
diff --git a/utils/jellyfin.ts b/utils/jellyfin.ts
index 4d930dec..2ab20bd8 100644
--- a/utils/jellyfin.ts
+++ b/utils/jellyfin.ts
@@ -15,7 +15,35 @@ import { useAtom } from "jotai";
import { useCallback, useRef, useState } from "react";
import { runningProcesses } from "./atoms/downloads";
import { iosProfile } from "./device-profiles";
-import { apiAtom } from "@/providers/JellyfinProvider";
+import { FFmpegKit, ReturnCode } from "ffmpeg-kit-react-native";
+
+const convertAndReplaceVideo = async (inputUri: string) => {
+ const tempOutputUri = inputUri.replace(/\.\w+$/, "_temp.mp4");
+
+ // Strip the file:/// prefix
+ const inputPath = inputUri.replace("file://", "");
+ const tempOutputPath = tempOutputUri.replace("file://", "");
+
+ const command = `-i ${inputPath} -c:v libx264 -profile:v baseline -level 3.0 -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart ${tempOutputPath}`;
+ try {
+ const session = await FFmpegKit.execute(command);
+ const rc: ReturnCode = await session.getReturnCode();
+ if (ReturnCode.isSuccess(rc)) {
+ console.log("Conversion successful, replacing the original file");
+
+ await FileSystem.moveAsync({
+ from: tempOutputUri,
+ to: inputUri,
+ });
+
+ console.log("Replacement successful");
+ } else {
+ console.log("Conversion failed");
+ }
+ } catch (error) {
+ console.error("Error during conversion", error);
+ }
+};
export const useDownloadMedia = (api: Api | null, userId?: string | null) => {
const [isDownloading, setIsDownloading] = useState(false);
@@ -32,41 +60,24 @@ export const useDownloadMedia = (api: Api | null, userId?: string | null) => {
return false;
}
+ console.log("MediaSources: ", JSON.stringify(item.MediaSources));
+ console.log("MediaStreams: ", JSON.stringify(item.MediaStreams));
+
setIsDownloading(true);
setError(null);
+ setProgress({
+ item,
+ progress: 0,
+ });
const itemId = item.Id;
- console.info("Downloading media item", item);
-
- // const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
- // itemId,
- // userId: userId,
- // });
-
- // const url = await getStreamUrl({
- // api,
- // userId: userId,
- // item,
- // startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
- // sessionData: playbackData.data,
- // });
-
- // if (!url) {
- // setError("Could not get stream URL");
- // setIsDownloading(false);
- // setProgress(null);
- // return false;
- // }
-
try {
- const filename = `${itemId}.mp4`;
+ const filename = `${itemId}`;
const fileUri = `${FileSystem.documentDirectory}${filename}`;
const url = `${api.basePath}/Items/${itemId}/Download`;
- console.info("Starting download of media item from URL", url);
-
downloadResumableRef.current = FileSystem.createDownloadResumable(
url,
fileUri,
@@ -106,6 +117,8 @@ export const useDownloadMedia = (api: Api | null, userId?: string | null) => {
JSON.stringify(updatedFiles)
);
+ await convertAndReplaceVideo(fileUri);
+
setIsDownloading(false);
setProgress(null);
return true;