Skip to content

Migrate ServerDiscovery into the SDK #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,51 @@
"version" : "2.1.6"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "0f54d58bb5db9e064f332e8524150de379d1e51c",
"version" : "2.82.1"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "cd1e89816d345d2523b11c55654570acd5cd4c56",
"version" : "1.24.0"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1",
"version" : "1.4.2"
}
},
{
"identity" : "urlqueryencoder",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/kean/Get", from: "2.1.6"),
.package(url: "https://github.com/CreateAPI/URLQueryEncoder", from: "0.2.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.17.2"),
],
targets: [
.target(
name: "JellyfinAPI",
dependencies: [
.product(name: "Get", package: "Get"),
.product(name: "URLQueryEncoder", package: "URLQueryEncoder"),
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
],
path: "Sources",
exclude: [
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,51 @@ let quickConnectState = Task {
quickConnect.start()
```

## Server Discovery

The `ServerDiscovery` class allows you to discover Jellyfin servers on your local network using UDP broadcast. It shoudl work on both IPv4 and IPv6 networks to maximize discovery capabilities.

```swift
/// Create a ServerDiscovery instance
let discovery = ServerDiscovery()

/// Subscribe to discovered servers
discovery.discoveredServers
.receive(on: RunLoop.main)
.sink { server in
print("Found server: \(server.name) at \(server.url)")
/// Handle the found servers as needed
}
.store(in: &cancellables)

/// Track discovery state changes
discovery.state
.receive(on: RunLoop.main)
.sink { state in
switch state {
case .active:
print("Discovery in progress...")
case .inactive:
print("Discovery inactive")
case .error(let message):
print("Discovery error: \(message)")
}
}
.store(in: &cancellables)

/// Broadcast to the network then listen for a response for 6 seconds afterwards
/// Calling this again will reuse the same bindings for IPv4 and IPv6
discovery.broadcast(duration: 6)

/// Reset all of the bindings for IPv4 and IPv6
discovery.reset()
```

## Generation

```bash
# Download latest spec and run CreateAPI
$ make update
```

Alternatively, you can generate your own Swift Jellyfin SDK using [CreateAPI](https://github.com/CreateAPI/CreateAPI) or any other OpenAPI generator.
Alternatively, you can generate your own Swift Jellyfin SDK using [CreateAPI](https://github.com/CreateAPI/CreateAPI) or any other OpenAPI generator.
78 changes: 78 additions & 0 deletions Sources/Extensions/ServerDiscoveryResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// jellyfin-sdk-swift is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//

import Foundation

/// A server response received during UDP-based Jellyfin discovery.
///
/// This structure is parsed from incoming UDP JSON messages sent by Jellyfin servers.
/// It includes identifying and connection details needed to connect to the discovered instance.
public struct ServerDiscoveryResponse: Codable, Hashable, Identifiable {

/// The raw address string (e.g. "http://192.168.1.42:8096").
private let address: String

/// The unique server ID provided by the Jellyfin instance.
public let id: String

/// The server's user-friendly display name.
public let name: String

/// A computed URL from the raw address.
///
/// Falls back to `nil` if casting fails.
public var url: URL? {
URL(string: address)
}

/// Extracts the hostname from the address (e.g., "192.168.1.42").
///
/// Falls back to the raw address string if parsing fails.
public var host: String {
URLComponents(string: address)?.host ?? address
}

/// Extracts the port from the address (e.g., 8096).
///
/// If no port is specified in the address, determines based on scheme:
/// - HTTPS: 443
/// - HTTP: 80
/// - Other/None: 8096 - Jellyfin default http
public var port: Int {
guard let components = URLComponents(string: address) else {
return 8096
}

/// Return the port if found
if let port = components.port {
return port
}

/// Check the scheme if there is no explicit port
switch components.scheme?.lowercased() {
case "https":
return 443
case "http":
return 80
default:
return 8096
}
}

/// Maps incoming JSON keys to Swift properties.
public enum CodingKeys: String, CodingKey {
case address = "Address"
case id = "Id"
case name = "Name"
}

/// Ensures stable hash identity based on `id` only.
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Loading