Add WebSocketClient and model extensions
This commit is contained in:
parent
a58c5129fc
commit
20096beb4f
2 changed files with 124 additions and 0 deletions
31
Sources/LuminateCore/BaseItemDto+Display.swift
Normal file
31
Sources/LuminateCore/BaseItemDto+Display.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
|
||||
public extension Components.Schemas.BaseItemDto {
|
||||
var isMovie: Bool { _Type?.value1 == .Movie }
|
||||
var isSeries: Bool { _Type?.value1 == .Series }
|
||||
var isEpisode: Bool { _Type?.value1 == .Episode }
|
||||
var isSeason: Bool { _Type?.value1 == .Season }
|
||||
var isFolder: Bool { IsFolder ?? false }
|
||||
|
||||
var runtimeString: String {
|
||||
guard let ticks = RunTimeTicks else { return "" }
|
||||
let totalSeconds = Int(ticks / 10_000_000)
|
||||
let hours = totalSeconds / 3600
|
||||
let minutes = (totalSeconds % 3600) / 60
|
||||
if hours > 0 { return "\(hours)h \(minutes)m" }
|
||||
return "\(minutes)m"
|
||||
}
|
||||
|
||||
var yearString: String {
|
||||
guard let year = ProductionYear else { return "" }
|
||||
return "\(year)"
|
||||
}
|
||||
|
||||
var primaryImageTag: String? {
|
||||
ImageTags?.additionalProperties["Primary"]
|
||||
}
|
||||
|
||||
var backdropImageTag: String? {
|
||||
BackdropImageTags?.first
|
||||
}
|
||||
}
|
||||
93
Sources/LuminateCore/WebSocketClient.swift
Normal file
93
Sources/LuminateCore/WebSocketClient.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
public actor WebSocketClient {
|
||||
private var task: URLSessionWebSocketTask?
|
||||
private let session: URLSession
|
||||
private let serverURL: URL
|
||||
private let token: String
|
||||
private var isConnected = false
|
||||
|
||||
public init(serverURL: URL, token: String) {
|
||||
self.serverURL = serverURL
|
||||
self.token = token
|
||||
self.session = URLSession(configuration: .default)
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
|
||||
components.scheme = serverURL.scheme == "https" ? "wss" : "ws"
|
||||
components.path = "/socket"
|
||||
components.queryItems = [.init(name: "api_key", value: token)]
|
||||
guard let url = components.url else { return }
|
||||
task = session.webSocketTask(with: url)
|
||||
task?.resume()
|
||||
isConnected = true
|
||||
receiveMessage()
|
||||
}
|
||||
|
||||
public func disconnect() {
|
||||
task?.cancel(with: .goingAway, reason: nil)
|
||||
isConnected = false
|
||||
}
|
||||
|
||||
private func receiveMessage() {
|
||||
task?.receive { [weak self] result in
|
||||
Task { [weak self] in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
await self?.handleMessage(message)
|
||||
await self?.receiveMessage()
|
||||
case .failure:
|
||||
await self?.reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
|
||||
switch message {
|
||||
case .string(let text):
|
||||
guard let data = text.data(using: .utf8),
|
||||
let event = try? JSONDecoder().decode(WebSocketEvent.self, from: data) else { return }
|
||||
Task { @MainActor in
|
||||
NotificationCenter.default.post(
|
||||
name: .init(event.messageType),
|
||||
object: event
|
||||
)
|
||||
}
|
||||
case .data:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func reconnect() async {
|
||||
guard isConnected else { return }
|
||||
try? await Task.sleep(nanoseconds: 5_000_000_000)
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
public struct WebSocketEvent: Decodable {
|
||||
public let messageType: String
|
||||
public let data: [String: AnyDecodable]?
|
||||
}
|
||||
|
||||
public struct AnyDecodable: Decodable {
|
||||
public var value: Any
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let intVal = try? container.decode(Int.self) { value = intVal }
|
||||
else if let doubleVal = try? container.decode(Double.self) { value = doubleVal }
|
||||
else if let boolVal = try? container.decode(Bool.self) { value = boolVal }
|
||||
else if let stringVal = try? container.decode(String.self) { value = stringVal }
|
||||
else if let arrayVal = try? container.decode([AnyDecodable].self) { value = arrayVal.map(\.value) }
|
||||
else if let dictVal = try? container.decode([String: AnyDecodable].self) { value = dictVal.mapValues(\.value) }
|
||||
else { throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "AnyDecodable error")) }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue