diff --git a/lib/components/LoginScreen/login_flow.dart b/lib/components/LoginScreen/login_flow.dart index 4b5dd9da7..becbb17b6 100644 --- a/lib/components/LoginScreen/login_flow.dart +++ b/lib/components/LoginScreen/login_flow.dart @@ -290,6 +290,7 @@ class JellyfinServerClientDiscovery { .finest("Received datagram: ${utf8.decode(datagram.data)}"); final response = ClientDiscoveryResponse.fromJson( jsonDecode(utf8.decode(datagram.data))); + _clientDiscoveryLogger.fine("Received discovery response from ${datagram.address}:${datagram.port}: ${jsonEncode(response)}"); onServerFound(response); } } diff --git a/lib/main.dart b/lib/main.dart index e76a12faf..9853d8d26 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:finamp/services/keep_screen_on_helper.dart'; import 'package:finamp/services/offline_listen_helper.dart'; import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; +import 'package:finamp/services/server_discovery_emulation_service.dart'; import 'package:finamp/services/theme_provider.dart'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; @@ -340,6 +341,7 @@ Future _setupPlaybackServices() async { await GetIt.instance().initializePlayer(); GetIt.instance.registerSingleton(PlaybackHistoryService()); GetIt.instance.registerSingleton(AudioServiceHelper()); + GetIt.instance.registerSingleton(JellyfinServerDiscoveryEmulationService()); } /// Migrates the old DownloadLocations list to a map diff --git a/lib/models/jellyfin_models.dart b/lib/models/jellyfin_models.dart index 03907a2fa..f4a8b9675 100644 --- a/lib/models/jellyfin_models.dart +++ b/lib/models/jellyfin_models.dart @@ -3802,6 +3802,7 @@ class ClientDiscoveryResponse { factory ClientDiscoveryResponse.fromJson(Map json) => _$ClientDiscoveryResponseFromJson(json); + Map toJson() => _$ClientDiscoveryResponseToJson(this); } /// LyricMetadata model. diff --git a/lib/services/server_discovery_emulation_service.dart b/lib/services/server_discovery_emulation_service.dart new file mode 100644 index 000000000..28aa1808f --- /dev/null +++ b/lib/services/server_discovery_emulation_service.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:finamp/models/jellyfin_models.dart'; +import 'package:finamp/services/finamp_user_helper.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; + +/// Used for discovering Jellyfin servers on the local network +/// https://jellyfin.org/docs/general/networking/#port-bindings +/// For some reason it's always being referred to as "client discovery" in the Jellyfin docs, even though we're actually discovering servers +class JellyfinServerDiscoveryEmulationService { + static final _serverDiscoveryEmulationLogger = Logger("JellyfinServerDiscoveryEmulation"); + + final _finampUserHelper = GetIt.instance(); + + late RawDatagramSocket socket; + bool isDisposed = false; + + JellyfinServerDiscoveryEmulationService() { + advertiseServer(); + } + + void advertiseServer() async { + + const discoveryMessage = + "who is JellyfinServer?"; // doesn't seem to be case sensitive, but the Kotlin SDK uses this capitalization + final broadcastAddress = + InternetAddress("255.255.255.255"); // UDP broadcast address + const discoveryPort = 7359; // Jellyfin client discovery port + + socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, discoveryPort); + socket.broadcastEnabled = + true; // important to allow sending to broadcast address + socket.multicastHops = 5; // to account for weird network setups + + _serverDiscoveryEmulationLogger.fine("Advertising server on port $discoveryPort"); + + socket.listen((event) { + if (event == RawSocketEvent.read) { + final datagram = socket.receive(); + if (datagram != null) { + _serverDiscoveryEmulationLogger + .finest("Received datagram: ${utf8.decode(datagram.data)}"); + final requestMessage = utf8.decode(datagram.data); + if (requestMessage.toLowerCase().contains(discoveryMessage.toLowerCase())) { + _serverDiscoveryEmulationLogger.fine("Received discovery message from ${datagram.address}:${datagram.port}"); + // Respond with the server's information + final response = ClientDiscoveryResponse( + address: _finampUserHelper.currentUser?.baseUrl, + endpointAddress: _finampUserHelper.currentUser?.baseUrl, + id: _finampUserHelper.currentUser?.serverId, + name: "Jellyfin Server (provided by Finamp)", + ); + final responseMessage = jsonEncode(response); + _serverDiscoveryEmulationLogger.finest("Sending discovery response: $responseMessage"); + socket.send(utf8.encode(responseMessage), datagram.address, datagram.port); + _serverDiscoveryEmulationLogger.fine("Sent discovery response to ${datagram.address}:${datagram.port}"); + } + } + } + }); + } + + void dispose() { + isDisposed = true; + socket.close(); + } +}