jellyflood/Shared/Services/XtreamAPIClient.swift

215 lines
6.5 KiB
Swift

//
// 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 Foundation
/// Xtream Codes API Client for Live TV and VOD streams
class XtreamAPIClient {
private let server: XtreamServer
init(server: XtreamServer) {
self.server = server
}
// MARK: - Authentication
/// Server info response from Xtream API
struct ServerInfo: Codable {
let userInfo: UserInfo?
let serverInfo: ServerDetails?
struct UserInfo: Codable {
let username: String?
let password: String?
let message: String?
let auth: Int?
let status: String?
let exp_date: String?
let isTrial: String?
let activeCons: String?
let createdAt: String?
let maxConnections: String?
enum CodingKeys: String, CodingKey {
case username
case password
case message
case auth
case status
case exp_date
case isTrial = "is_trial"
case activeCons = "active_cons"
case createdAt = "created_at"
case maxConnections = "max_connections"
}
}
struct ServerDetails: Codable {
let url: String?
let port: String?
let httpsPort: String?
let serverProtocol: String?
let rtmpPort: String?
let timezone: String?
let timestampNow: Int?
let timeNow: String?
enum CodingKeys: String, CodingKey {
case url
case port
case timezone
case httpsPort = "https_port"
case serverProtocol = "server_protocol"
case rtmpPort = "rtmp_port"
case timestampNow = "timestamp_now"
case timeNow = "time_now"
}
}
enum CodingKeys: String, CodingKey {
case userInfo = "user_info"
case serverInfo = "server_info"
}
}
/// Test connection to Xtream server and authenticate
func testConnection() async throws -> ServerInfo {
guard let url = server.authenticatedURL(parameters: []) else {
throw XtreamAPIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 10
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw XtreamAPIError.invalidResponse
}
guard httpResponse.statusCode == 200 else {
throw XtreamAPIError.httpError(statusCode: httpResponse.statusCode)
}
let serverInfo = try JSONDecoder().decode(ServerInfo.self, from: data)
// Check if authentication was successful
if let auth = serverInfo.userInfo?.auth, auth == 0 {
throw XtreamAPIError.authenticationFailed(message: serverInfo.userInfo?.message ?? "Authentication failed")
}
return serverInfo
}
// MARK: - Live Channels
struct LiveCategory: Codable, Identifiable {
let categoryId: String
let categoryName: String
let parentId: Int?
var id: String { categoryId }
enum CodingKeys: String, CodingKey {
case categoryId = "category_id"
case categoryName = "category_name"
case parentId = "parent_id"
}
}
struct LiveChannel: Codable, Identifiable {
let num: Int?
let name: String?
let streamType: String?
let streamId: Int?
let streamIcon: String?
let epgChannelId: String?
let added: String?
let categoryId: String?
let customSid: String?
let tvArchive: Int?
let directSource: String?
let tvArchiveDuration: Int?
var id: Int { streamId ?? 0 }
enum CodingKeys: String, CodingKey {
case num
case name
case added
case streamType = "stream_type"
case streamId = "stream_id"
case streamIcon = "stream_icon"
case epgChannelId = "epg_channel_id"
case categoryId = "category_id"
case customSid = "custom_sid"
case tvArchive = "tv_archive"
case directSource = "direct_source"
case tvArchiveDuration = "tv_archive_duration"
}
}
/// Get live TV categories
func getLiveCategories() async throws -> [LiveCategory] {
guard let url = server.authenticatedURL(parameters: [
URLQueryItem(name: "action", value: "get_live_categories"),
]) else {
throw XtreamAPIError.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([LiveCategory].self, from: data)
}
/// Get live TV channels
func getLiveChannels(categoryId: String? = nil) async throws -> [LiveChannel] {
var parameters = [URLQueryItem(name: "action", value: "get_live_streams")]
if let categoryId {
parameters.append(URLQueryItem(name: "category_id", value: categoryId))
}
guard let url = server.authenticatedURL(parameters: parameters) else {
throw XtreamAPIError.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([LiveChannel].self, from: data)
}
/// Get live stream URL for a channel
func getLiveStreamURL(streamId: Int) -> URL? {
let urlString = "\(server.url.absoluteString)/live/\(server.username)/\(server.password)/\(streamId).ts"
return URL(string: urlString)
}
}
// MARK: - Errors
enum XtreamAPIError: LocalizedError {
case invalidURL
case invalidResponse
case httpError(statusCode: Int)
case authenticationFailed(message: String)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid Xtream server URL"
case .invalidResponse:
return "Invalid response from Xtream server"
case let .httpError(statusCode):
return "HTTP error: \(statusCode)"
case let .authenticationFailed(message):
return "Authentication failed: \(message)"
}
}
}