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;