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