Skip to content

Commit b265d9f

Browse files
committed
proof-of-concept for server discovery emulation
- currently always on
1 parent 872e1bc commit b265d9f

File tree

4 files changed

+73
-0
lines changed

4 files changed

+73
-0
lines changed

lib/components/LoginScreen/login_flow.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class JellyfinServerClientDiscovery {
290290
.finest("Received datagram: ${utf8.decode(datagram.data)}");
291291
final response = ClientDiscoveryResponse.fromJson(
292292
jsonDecode(utf8.decode(datagram.data)));
293+
_clientDiscoveryLogger.fine("Received discovery response from ${datagram.address}:${datagram.port}: ${jsonEncode(response)}");
293294
onServerFound(response);
294295
}
295296
}

lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:finamp/services/keep_screen_on_helper.dart';
2020
import 'package:finamp/services/offline_listen_helper.dart';
2121
import 'package:finamp/services/playback_history_service.dart';
2222
import 'package:finamp/services/queue_service.dart';
23+
import 'package:finamp/services/server_discovery_emulation_service.dart';
2324
import 'package:finamp/services/theme_provider.dart';
2425
import 'package:audio_service/audio_service.dart';
2526
import 'package:audio_session/audio_session.dart';
@@ -340,6 +341,7 @@ Future<void> _setupPlaybackServices() async {
340341
await GetIt.instance<QueueService>().initializePlayer();
341342
GetIt.instance.registerSingleton(PlaybackHistoryService());
342343
GetIt.instance.registerSingleton(AudioServiceHelper());
344+
GetIt.instance.registerSingleton(JellyfinServerDiscoveryEmulationService());
343345
}
344346

345347
/// Migrates the old DownloadLocations list to a map

lib/models/jellyfin_models.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3802,6 +3802,7 @@ class ClientDiscoveryResponse {
38023802

38033803
factory ClientDiscoveryResponse.fromJson(Map<String, dynamic> json) =>
38043804
_$ClientDiscoveryResponseFromJson(json);
3805+
Map<String, dynamic> toJson() => _$ClientDiscoveryResponseToJson(this);
38053806
}
38063807

38073808
/// LyricMetadata model.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:finamp/models/jellyfin_models.dart';
5+
import 'package:finamp/services/finamp_user_helper.dart';
6+
import 'package:get_it/get_it.dart';
7+
import 'package:logging/logging.dart';
8+
9+
/// Used for discovering Jellyfin servers on the local network
10+
/// https://jellyfin.org/docs/general/networking/#port-bindings
11+
/// For some reason it's always being referred to as "client discovery" in the Jellyfin docs, even though we're actually discovering servers
12+
class JellyfinServerDiscoveryEmulationService {
13+
static final _serverDiscoveryEmulationLogger = Logger("JellyfinServerDiscoveryEmulation");
14+
15+
final _finampUserHelper = GetIt.instance<FinampUserHelper>();
16+
17+
late RawDatagramSocket socket;
18+
bool isDisposed = false;
19+
20+
JellyfinServerDiscoveryEmulationService() {
21+
advertiseServer();
22+
}
23+
24+
void advertiseServer() async {
25+
26+
const discoveryMessage =
27+
"who is JellyfinServer?"; // doesn't seem to be case sensitive, but the Kotlin SDK uses this capitalization
28+
final broadcastAddress =
29+
InternetAddress("255.255.255.255"); // UDP broadcast address
30+
const discoveryPort = 7359; // Jellyfin client discovery port
31+
32+
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, discoveryPort);
33+
socket.broadcastEnabled =
34+
true; // important to allow sending to broadcast address
35+
socket.multicastHops = 5; // to account for weird network setups
36+
37+
_serverDiscoveryEmulationLogger.fine("Advertising server on port $discoveryPort");
38+
39+
socket.listen((event) {
40+
if (event == RawSocketEvent.read) {
41+
final datagram = socket.receive();
42+
if (datagram != null) {
43+
_serverDiscoveryEmulationLogger
44+
.finest("Received datagram: ${utf8.decode(datagram.data)}");
45+
final requestMessage = utf8.decode(datagram.data);
46+
if (requestMessage.toLowerCase().contains(discoveryMessage.toLowerCase())) {
47+
_serverDiscoveryEmulationLogger.fine("Received discovery message from ${datagram.address}:${datagram.port}");
48+
// Respond with the server's information
49+
final response = ClientDiscoveryResponse(
50+
address: _finampUserHelper.currentUser?.baseUrl,
51+
endpointAddress: _finampUserHelper.currentUser?.baseUrl,
52+
id: _finampUserHelper.currentUser?.serverId,
53+
name: "Jellyfin Server (provided by Finamp)",
54+
);
55+
final responseMessage = jsonEncode(response);
56+
_serverDiscoveryEmulationLogger.finest("Sending discovery response: $responseMessage");
57+
socket.send(utf8.encode(responseMessage), datagram.address, datagram.port);
58+
_serverDiscoveryEmulationLogger.fine("Sent discovery response to ${datagram.address}:${datagram.port}");
59+
}
60+
}
61+
}
62+
});
63+
}
64+
65+
void dispose() {
66+
isDisposed = true;
67+
socket.close();
68+
}
69+
}

0 commit comments

Comments
 (0)