forked from Ninjalama/streamyfin_mirror
Merge pull request #416 from streamyfin/feat/server-discovery
feat: server discovery during login
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
|
||||
import { PreviousServersList } from "@/components/PreviousServersList";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
Ionicons,
|
||||
MaterialCommunityIcons,
|
||||
MaterialIcons,
|
||||
} from "@expo/vector-icons";
|
||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
@@ -313,6 +309,15 @@ const Login: React.FC = () => {
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<JellyfinServerDiscovery
|
||||
onServerSelect={(server) => {
|
||||
setServerURL(server.address);
|
||||
if (server.serverName) {
|
||||
setServerName(server.serverName);
|
||||
}
|
||||
handleConnect(server.address);
|
||||
}}
|
||||
/>
|
||||
<PreviousServersList
|
||||
onServerSelect={(s) => {
|
||||
handleConnect(s.address);
|
||||
|
||||
44
components/JellyfinServerDiscovery.tsx
Normal file
44
components/JellyfinServerDiscovery.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
|
||||
import { Button } from "./Button";
|
||||
import { ListGroup } from "./list/ListGroup";
|
||||
import { ListItem } from "./list/ListItem";
|
||||
|
||||
interface Props {
|
||||
onServerSelect?: (server: { address: string; serverName?: string }) => void;
|
||||
}
|
||||
|
||||
const JellyfinServerDiscovery: React.FC<Props> = ({ onServerSelect }) => {
|
||||
const { servers, isSearching, startDiscovery } = useJellyfinDiscovery();
|
||||
|
||||
return (
|
||||
<View className="mt-2">
|
||||
<Button onPress={startDiscovery} color="black">
|
||||
<Text className="text-white text-center">
|
||||
{isSearching ? "Searching..." : "Search for local servers"}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{servers.length ? (
|
||||
<ListGroup title="Servers" className="mt-4">
|
||||
{servers.map((server) => (
|
||||
<ListItem
|
||||
key={server.address}
|
||||
onPress={() =>
|
||||
onServerSelect?.({
|
||||
address: server.address,
|
||||
serverName: server.serverName,
|
||||
})
|
||||
}
|
||||
title={server.address}
|
||||
showArrow
|
||||
/>
|
||||
))}
|
||||
</ListGroup>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default JellyfinServerDiscovery;
|
||||
106
hooks/useJellyfinDiscovery.tsx
Normal file
106
hooks/useJellyfinDiscovery.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import dgram from "react-native-udp";
|
||||
|
||||
const JELLYFIN_DISCOVERY_PORT = 7359;
|
||||
const DISCOVERY_MESSAGE = "Who is JellyfinServer?";
|
||||
|
||||
interface ServerInfo {
|
||||
address: string;
|
||||
port: number;
|
||||
serverId?: string;
|
||||
serverName?: string;
|
||||
}
|
||||
|
||||
export const useJellyfinDiscovery = () => {
|
||||
const [servers, setServers] = useState<ServerInfo[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const startDiscovery = useCallback(() => {
|
||||
setIsSearching(true);
|
||||
setServers([]);
|
||||
|
||||
const discoveredServers = new Set<string>();
|
||||
let discoveryTimeout: NodeJS.Timeout;
|
||||
|
||||
const socket = dgram.createSocket({
|
||||
type: "udp4",
|
||||
reusePort: true,
|
||||
debug: __DEV__,
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
console.error("Socket error:", err);
|
||||
socket.close();
|
||||
setIsSearching(false);
|
||||
});
|
||||
|
||||
socket.bind(0, () => {
|
||||
console.log("UDP socket bound successfully");
|
||||
|
||||
try {
|
||||
socket.setBroadcast(true);
|
||||
const messageBuffer = new TextEncoder().encode(DISCOVERY_MESSAGE);
|
||||
|
||||
socket.send(
|
||||
messageBuffer,
|
||||
0,
|
||||
messageBuffer.length,
|
||||
JELLYFIN_DISCOVERY_PORT,
|
||||
"255.255.255.255",
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error("Failed to send discovery message:", err);
|
||||
return;
|
||||
}
|
||||
console.log("Discovery message sent successfully");
|
||||
}
|
||||
);
|
||||
|
||||
discoveryTimeout = setTimeout(() => {
|
||||
setIsSearching(false);
|
||||
socket.close();
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error("Error during discovery:", error);
|
||||
setIsSearching(false);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("message", (msg, rinfo: any) => {
|
||||
if (discoveredServers.has(rinfo.address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = new TextDecoder().decode(msg);
|
||||
const serverInfo = JSON.parse(response);
|
||||
discoveredServers.add(rinfo.address);
|
||||
|
||||
const newServer: ServerInfo = {
|
||||
address: `http://${rinfo.address}:${serverInfo.Port || 8096}`,
|
||||
port: serverInfo.Port || 8096,
|
||||
serverId: serverInfo.Id,
|
||||
serverName: serverInfo.Name,
|
||||
};
|
||||
|
||||
setServers((prev) => [...prev, newServer]);
|
||||
} catch (error) {
|
||||
console.error("Error parsing server response:", error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(discoveryTimeout);
|
||||
if (isSearching) {
|
||||
setIsSearching(false);
|
||||
}
|
||||
socket.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
servers,
|
||||
isSearching,
|
||||
startDiscovery,
|
||||
};
|
||||
};
|
||||
@@ -94,6 +94,7 @@
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-svg": "15.2.0",
|
||||
"react-native-tab-view": "^3.5.2",
|
||||
"react-native-udp": "^4.1.7",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
|
||||
@@ -72,7 +72,13 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
const [isPolling, setIsPolling] = useState<boolean>(false);
|
||||
const [secret, setSecret] = useState<string | null>(null);
|
||||
const [settings, updateSettings, pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings] = useSettings();
|
||||
const [
|
||||
settings,
|
||||
updateSettings,
|
||||
pluginSettings,
|
||||
setPluginSettings,
|
||||
refreshStreamyfinPluginSettings,
|
||||
] = useSettings();
|
||||
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr();
|
||||
|
||||
useQuery({
|
||||
@@ -233,12 +239,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
|
||||
const recentPluginSettings = await refreshStreamyfinPluginSettings();
|
||||
if (recentPluginSettings?.jellyseerrServerUrl?.value) {
|
||||
const jellyseerrApi = new JellyseerrApi(recentPluginSettings.jellyseerrServerUrl.value);
|
||||
const jellyseerrApi = new JellyseerrApi(
|
||||
recentPluginSettings.jellyseerrServerUrl.value
|
||||
);
|
||||
await jellyseerrApi.test().then((result) => {
|
||||
if (result.isValid && result.requiresPass) {
|
||||
jellyseerrApi.login(username, password).then(setJellyseerrUser);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user