forked from Ninjalama/streamyfin_mirror
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e433afd4d | ||
|
|
3e1fd5a0ad | ||
|
|
0ae8a0a58c | ||
|
|
34d9392a8b | ||
|
|
1b463382c5 | ||
|
|
4b94bd33ce | ||
|
|
315d9cbc63 | ||
|
|
d939f7c9e3 | ||
|
|
4d5e544fb0 | ||
|
|
5e17f2ac88 | ||
|
|
74fa279f8d | ||
|
|
4382e585fe | ||
|
|
a9486c57d2 | ||
|
|
da9ac3efde | ||
|
|
7bab4a78bc | ||
|
|
5f323d5132 | ||
|
|
18152b9d5b | ||
|
|
6b69250ecb | ||
|
|
89a992e7c1 |
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone15Pro]
|
||||
- OS: [e.g. iOS18]
|
||||
- Version [e.g. 0.3.1]
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ Streamyfin.app
|
||||
|
||||
pc-api-7079014811501811218-719-3b9f15aeccf8.json
|
||||
credentials.json
|
||||
development.apk
|
||||
Streamyfin.apk
|
||||
Streamyfin.ipa
|
||||
|
||||
@@ -21,6 +21,10 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
|
||||
|
||||
Streamyfin includes some exciting experimental features like media downloading and Chromecast support. These are still in development, and we appreciate your patience and feedback as we work to improve them.
|
||||
|
||||
### Downloading
|
||||
|
||||
Downloading works by using ffmpeg to convert a HLS stream into a video file on the device. This means that you can download and view any file you can stream! The file is converted by Jellyfin on the server in real time as it is downloaded. This means a **bit longer download times** but supports any file that your server can transcode.
|
||||
|
||||
## 🛠️ Beta testing (iOS/Android)
|
||||
|
||||
## TestFlight
|
||||
@@ -93,6 +97,11 @@ If you have questions or need support, feel free to reach out:
|
||||
|
||||
- GitHub Issues: Report bugs or request features here.
|
||||
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
|
||||
-
|
||||
|
||||
## Support
|
||||
|
||||
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
## 📝 Credits
|
||||
|
||||
|
||||
25
app.json
25
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Streamyfin",
|
||||
"slug": "streamyfin",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.2",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
@@ -10,11 +10,12 @@
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
"backgroundColor": "#29164B"
|
||||
},
|
||||
"jsEngine": "hermes",
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"requireFullScreen": true,
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "The app needs access to your camera to scan barcodes.",
|
||||
"NSMicrophoneUsageDescription": "The app needs access to your microphone."
|
||||
@@ -23,7 +24,9 @@
|
||||
"bundleIdentifier": "com.fredrikburmester.streamyfin"
|
||||
},
|
||||
"android": {
|
||||
"jsEngine": "jsc",
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 10,
|
||||
"orientation": "default",
|
||||
"androidNavigationBar": {
|
||||
"visible": true,
|
||||
"barStyle": "dark-content",
|
||||
@@ -36,8 +39,7 @@
|
||||
"permissions": [
|
||||
"android.permission.FOREGROUND_SERVICE",
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
|
||||
],
|
||||
"versionCode": 6
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
@@ -75,6 +77,7 @@
|
||||
},
|
||||
"android": {
|
||||
"minSdkVersion": 24,
|
||||
"usesCleartextTraffic": true,
|
||||
"packagingOptions": {
|
||||
"jniLibs": {
|
||||
"useLegacyPackaging": true
|
||||
@@ -82,6 +85,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-screen-orientation",
|
||||
{
|
||||
"initialOrientation": "DEFAULT"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-sensors",
|
||||
{
|
||||
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -26,7 +26,11 @@ 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 { useCastDevice } from "react-native-google-cast";
|
||||
import CastContext, {
|
||||
PlayServicesState,
|
||||
useCastDevice,
|
||||
useRemoteMediaClient,
|
||||
} from "react-native-google-cast";
|
||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import ios12 from "@/utils/profiles/ios12";
|
||||
import { currentlyPlayingItemAtom } from "@/components/CurrentlyPlayingBar";
|
||||
@@ -40,6 +44,8 @@ const page: React.FC = () => {
|
||||
|
||||
const castDevice = useCastDevice();
|
||||
|
||||
const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]);
|
||||
|
||||
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
|
||||
key: "Max",
|
||||
value: undefined,
|
||||
@@ -110,13 +116,36 @@ const page: React.FC = () => {
|
||||
});
|
||||
|
||||
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
|
||||
const client = useRemoteMediaClient();
|
||||
|
||||
const onPressPlay = useCallback(() => {
|
||||
const onPressPlay = useCallback(async () => {
|
||||
if (!playbackUrl || !item) return;
|
||||
setCp({
|
||||
item,
|
||||
playbackUrl,
|
||||
});
|
||||
|
||||
if (chromecastReady && 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,
|
||||
});
|
||||
}
|
||||
}, [playbackUrl, item]);
|
||||
|
||||
if (l1)
|
||||
@@ -222,7 +251,11 @@ const page: React.FC = () => {
|
||||
onChange={(val) => setMaxBitrate(val)}
|
||||
selected={maxBitrate}
|
||||
/>
|
||||
<PlayButton item={item} chromecastReady={false} onPress={onPressPlay} />
|
||||
<PlayButton
|
||||
item={item}
|
||||
chromecastReady={chromecastReady}
|
||||
onPress={onPressPlay}
|
||||
/>
|
||||
</View>
|
||||
<ScrollView horizontal className="flex px-4 mb-4">
|
||||
<View className="flex flex-row space-x-2 ">
|
||||
|
||||
@@ -2,22 +2,14 @@ import { JellyfinProvider } from "@/providers/JellyfinProvider";
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useFonts } from "expo-font";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { Stack, router } from "expo-router";
|
||||
import { Stack } from "expo-router";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { Provider as JotaiProvider } from "jotai";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Platform, TouchableOpacity } from "react-native";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import "react-native-reanimated";
|
||||
|
||||
import Feather from "@expo/vector-icons/Feather";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import Video from "react-native-video";
|
||||
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
@@ -46,14 +38,36 @@ export default function RootLayout() {
|
||||
}),
|
||||
);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
const [orientation, setOrientation] = useState(
|
||||
ScreenOrientation.Orientation.PORTRAIT_UP,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
|
||||
|
||||
ScreenOrientation.getOrientationAsync().then((info) => {
|
||||
setOrientation(info);
|
||||
});
|
||||
|
||||
// subscribe to future changes
|
||||
const subscription = ScreenOrientation.addOrientationChangeListener(
|
||||
(evt) => {
|
||||
setOrientation(evt.orientationInfo.orientation);
|
||||
},
|
||||
);
|
||||
|
||||
// return a clean up function to unsubscribe from notifications
|
||||
return () => {
|
||||
ScreenOrientation.removeOrientationChangeListener(subscription);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const Login: React.FC = () => {
|
||||
>
|
||||
<View className="flex flex-col px-4 justify-center h-full gap-y-2">
|
||||
<View>
|
||||
<Text className="text-3xl font-bold">Jellyfin</Text>
|
||||
<Text className="text-3xl font-bold">Streamyfin</Text>
|
||||
<Text className="opacity-50 mb-2">Server: {api.basePath}</Text>
|
||||
<Button
|
||||
color="black"
|
||||
@@ -137,15 +137,13 @@ const Login: React.FC = () => {
|
||||
>
|
||||
<View className="flex flex-col px-4 justify-center h-full">
|
||||
<View className="flex flex-col gap-y-2">
|
||||
<Text className="text-3xl font-bold">Jellyfin</Text>
|
||||
<Text className="text-3xl font-bold">Streamyfin</Text>
|
||||
<Text className="opacity-50">Enter a server adress</Text>
|
||||
<Input
|
||||
className="mb-2"
|
||||
placeholder="http(s)://..."
|
||||
onChangeText={setServerURL}
|
||||
value={serverURL}
|
||||
autoFocus
|
||||
secureTextEntry={false}
|
||||
keyboardType="url"
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
|
||||
9
eas.json
9
eas.json
@@ -21,10 +21,17 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"channel": "0.0.6",
|
||||
"channel": "0.3.1",
|
||||
"android": {
|
||||
"image": "latest"
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"channel": "0.3.1",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.51.16",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"axios": "^1.7.3",
|
||||
"expo": "~51.0.26",
|
||||
"expo-blur": "~13.0.2",
|
||||
"expo-build-properties": "~0.12.5",
|
||||
@@ -38,6 +39,8 @@
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-navigation-bar": "~3.0.7",
|
||||
"expo-router": "~3.5.21",
|
||||
"expo-screen-orientation": "~7.0.5",
|
||||
"expo-sensors": "~13.0.9",
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
|
||||
@@ -12,6 +12,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform } from "react-native";
|
||||
import uuid from "react-native-uuid";
|
||||
|
||||
interface Server {
|
||||
@@ -30,7 +31,7 @@ interface JellyfinContextValue {
|
||||
}
|
||||
|
||||
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
|
||||
const getOrSetDeviceId = async () => {
|
||||
@@ -56,8 +57,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
() =>
|
||||
new Jellyfin({
|
||||
clientInfo: { name: "Streamyfin", version: "1.0.0" },
|
||||
deviceInfo: { name: "iOS", id },
|
||||
})
|
||||
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
||||
}),
|
||||
);
|
||||
})();
|
||||
}, []);
|
||||
@@ -66,9 +67,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
|
||||
const discoverServers = async (url: string): Promise<Server[]> => {
|
||||
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
|
||||
url
|
||||
);
|
||||
const servers =
|
||||
await jellyfin?.discovery.getRecommendedServerCandidates(url);
|
||||
return servers?.map((server) => ({ address: server.address })) || [];
|
||||
};
|
||||
|
||||
@@ -144,7 +144,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const token = await AsyncStorage.getItem("token");
|
||||
const serverUrl = await AsyncStorage.getItem("serverUrl");
|
||||
const user = JSON.parse(
|
||||
(await AsyncStorage.getItem("user")) as string
|
||||
(await AsyncStorage.getItem("user")) as string,
|
||||
) as UserDto;
|
||||
|
||||
if (serverUrl && token && user.Id && jellyfin) {
|
||||
|
||||
Reference in New Issue
Block a user