forked from Ninjalama/streamyfin_mirror
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b94bd33ce | ||
|
|
315d9cbc63 | ||
|
|
d939f7c9e3 | ||
|
|
4d5e544fb0 | ||
|
|
5e17f2ac88 | ||
|
|
74fa279f8d | ||
|
|
4382e585fe | ||
|
|
a9486c57d2 | ||
|
|
da9ac3efde | ||
|
|
7bab4a78bc | ||
|
|
5f323d5132 | ||
|
|
18152b9d5b | ||
|
|
6b69250ecb | ||
|
|
89a992e7c1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,3 +27,5 @@ Streamyfin.app
|
||||
|
||||
pc-api-7079014811501811218-719-3b9f15aeccf8.json
|
||||
credentials.json
|
||||
development.apk
|
||||
Streamyfin.apk
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
7
app.json
7
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Streamyfin",
|
||||
"slug": "streamyfin",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
@@ -10,7 +10,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
"backgroundColor": "#29164B"
|
||||
},
|
||||
"jsEngine": "hermes",
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
@@ -37,7 +37,7 @@
|
||||
"android.permission.FOREGROUND_SERVICE",
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
|
||||
],
|
||||
"versionCode": 6
|
||||
"versionCode": 9
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
@@ -75,6 +75,7 @@
|
||||
},
|
||||
"android": {
|
||||
"minSdkVersion": 24,
|
||||
"usesCleartextTraffic": true,
|
||||
"packagingOptions": {
|
||||
"jniLibs": {
|
||||
"useLegacyPackaging": true
|
||||
|
||||
@@ -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 ">
|
||||
|
||||
@@ -144,8 +144,6 @@ const Login: React.FC = () => {
|
||||
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",
|
||||
|
||||
@@ -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