diff --git a/app/login.tsx b/app/login.tsx index 614ad793..9ac2d06c 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -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 + { + setServerURL(server.address); + if (server.serverName) { + setServerName(server.serverName); + } + handleConnect(server.address); + }} + /> { handleConnect(s.address); diff --git a/bun.lockb b/bun.lockb index df0e1988..935cb03c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/JellyfinServerDiscovery.tsx b/components/JellyfinServerDiscovery.tsx new file mode 100644 index 00000000..5c310d64 --- /dev/null +++ b/components/JellyfinServerDiscovery.tsx @@ -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 = ({ onServerSelect }) => { + const { servers, isSearching, startDiscovery } = useJellyfinDiscovery(); + + return ( + + + + {servers.length ? ( + + {servers.map((server) => ( + + onServerSelect?.({ + address: server.address, + serverName: server.serverName, + }) + } + title={server.address} + showArrow + /> + ))} + + ) : null} + + ); +}; + +export default JellyfinServerDiscovery; diff --git a/hooks/useJellyfinDiscovery.tsx b/hooks/useJellyfinDiscovery.tsx new file mode 100644 index 00000000..963dfe81 --- /dev/null +++ b/hooks/useJellyfinDiscovery.tsx @@ -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([]); + const [isSearching, setIsSearching] = useState(false); + + const startDiscovery = useCallback(() => { + setIsSearching(true); + setServers([]); + + const discoveredServers = new Set(); + 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, + }; +}; diff --git a/package.json b/package.json index 7c6463e0..07fedc03 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 2b602323..4455dfe1 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -72,7 +72,13 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [user, setUser] = useAtom(userAtom); const [isPolling, setIsPolling] = useState(false); const [secret, setSecret] = useState(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) {