215 lines
6.5 KiB
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)"
|
|
}
|
|
}
|
|
}
|