// // Swiftfin 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 Combine import Defaults import Foundation final class ConnectToXtreamViewModel: ViewModel, Eventful, Stateful { // MARK: Event enum Event { case connected(XtreamServer) case error(XtreamAPIError) } // MARK: Action enum Action: Equatable { case cancel case connect(name: String, url: String, username: String, password: String) case testConnection(name: String, url: String, username: String, password: String) } // MARK: State enum State: Hashable { case connecting case initial case testing } @Published var state: State = .initial var events: AnyPublisher { eventSubject .receive(on: RunLoop.main) .eraseToAnyPublisher() } private var connectTask: AnyCancellable? private var eventSubject: PassthroughSubject = .init() func respond(to action: Action) -> State { switch action { case .cancel: connectTask?.cancel() return .initial case let .connect(name, urlString, username, password): connectTask?.cancel() connectTask = Task { do { let server = try await connectToXtream( name: name, url: urlString, username: username, password: password ) await MainActor.run { self.eventSubject.send(.connected(server)) self.state = .initial } } catch is CancellationError { // cancel doesn't matter } catch let error as XtreamAPIError { await MainActor.run { self.eventSubject.send(.error(error)) self.state = .initial } } catch { await MainActor.run { self.eventSubject.send(.error(.invalidResponse)) self.state = .initial } } } .asAnyCancellable() return .connecting case let .testConnection(name, urlString, username, password): connectTask?.cancel() connectTask = Task { do { _ = try await connectToXtream( name: name, url: urlString, username: username, password: password ) await MainActor.run { self.state = .initial } } catch is CancellationError { // cancel doesn't matter } catch let error as XtreamAPIError { await MainActor.run { self.eventSubject.send(.error(error)) self.state = .initial } } catch { await MainActor.run { self.eventSubject.send(.error(.invalidResponse)) self.state = .initial } } } .asAnyCancellable() return .testing } } private func connectToXtream( name: String, url urlString: String, username: String, password: String ) async throws -> XtreamServer { let formattedURL = urlString.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: .objectReplacement) .trimmingCharacters(in: ["/"]) .prepending("http://", if: !urlString.contains("://")) guard let url = URL(string: formattedURL) else { throw XtreamAPIError.invalidURL } let server = XtreamServer( name: name.isEmpty ? "Xtream Server" : name, url: url, username: username, password: password ) // Test connection let client = XtreamAPIClient(server: server) _ = try await client.testConnection() return server } func saveServer(_ server: XtreamServer) { var servers = Defaults[.xtreamServers] // Check if server with same ID exists and update, otherwise append if let index = servers.firstIndex(where: { $0.id == server.id }) { servers[index] = server } else { servers.append(server) } Defaults[.xtreamServers] = servers // Set as current server if it's the first one if Defaults[.currentXtreamServerID] == nil { Defaults[.currentXtreamServerID] = server.id } } func deleteServer(_ server: XtreamServer) { var servers = Defaults[.xtreamServers] servers.removeAll { $0.id == server.id } Defaults[.xtreamServers] = servers // Clear current server if deleted if Defaults[.currentXtreamServerID] == server.id { Defaults[.currentXtreamServerID] = servers.first?.id } } }