// // LiveModels.swift // suixinkan // // Created by Codex on 2026/6/25. // import Foundation /// 直播列表响应实体。 struct LiveListResponse: Decodable { let items: [LiveEntity] let total: Int let page: Int let pageSize: Int enum CodingKeys: String, CodingKey { case items case total case page case pageSize = "page_size" } init(items: [LiveEntity] = [], total: Int = 0, page: Int = 1, pageSize: Int = 10) { self.items = items self.total = total self.page = page self.pageSize = pageSize } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) items = (try? container.decode([LiveEntity].self, forKey: .items)) ?? [] total = try container.liveDecodeLossyInt(forKey: .total) ?? 0 page = try container.liveDecodeLossyInt(forKey: .page) ?? 1 pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10 } } /// 直播实体,表示一场手动直播。 struct LiveEntity: Decodable, Identifiable, Equatable { let id: Int let title: String let coverImg: String let pushUrl: String let playUrl: String let pullUrl: String let hlsUrl: String let flvUrl: String let liveUrl: String let startTime: Int64 let endTime: Int64 let duration: Int64 let status: Int let statusLabel: String let manualPushMode: Int let manualPushState: Int let viewsCount: Int enum CodingKeys: String, CodingKey { case id case title case coverImg = "cover_img" case pushUrl = "push_url" case playUrl = "play_url" case pullUrl = "pull_url" case hlsUrl = "hls_url" case flvUrl = "flv_url" case liveUrl = "live_url" case startTime = "start_time" case endTime = "end_time" case duration case status case statusLabel = "status_label" case manualPushMode = "manaul_push_mode" case manualPushState = "manaul_push_state" case viewsCount = "views_count" } init( id: Int = 0, title: String = "", coverImg: String = "", pushUrl: String = "", playUrl: String = "", pullUrl: String = "", hlsUrl: String = "", flvUrl: String = "", liveUrl: String = "", startTime: Int64 = 0, endTime: Int64 = 0, duration: Int64 = 0, status: Int = 0, statusLabel: String = "", manualPushMode: Int = 1, manualPushState: Int = 0, viewsCount: Int = 0 ) { self.id = id self.title = title self.coverImg = coverImg self.pushUrl = pushUrl self.playUrl = playUrl self.pullUrl = pullUrl self.hlsUrl = hlsUrl self.flvUrl = flvUrl self.liveUrl = liveUrl self.startTime = startTime self.endTime = endTime self.duration = duration self.status = status self.statusLabel = statusLabel self.manualPushMode = manualPushMode self.manualPushState = manualPushState self.viewsCount = viewsCount } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.liveDecodeLossyInt(forKey: .id) ?? 0 title = try container.liveDecodeLossyString(forKey: .title) coverImg = try container.liveDecodeLossyString(forKey: .coverImg) pushUrl = try container.liveDecodeLossyString(forKey: .pushUrl) playUrl = try container.liveDecodeLossyString(forKey: .playUrl) pullUrl = try container.liveDecodeLossyString(forKey: .pullUrl) hlsUrl = try container.liveDecodeLossyString(forKey: .hlsUrl) flvUrl = try container.liveDecodeLossyString(forKey: .flvUrl) liveUrl = try container.liveDecodeLossyString(forKey: .liveUrl) startTime = Int64(try container.liveDecodeLossyInt(forKey: .startTime) ?? 0) endTime = Int64(try container.liveDecodeLossyInt(forKey: .endTime) ?? 0) duration = Int64(try container.liveDecodeLossyInt(forKey: .duration) ?? 0) status = try container.liveDecodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.liveDecodeLossyString(forKey: .statusLabel) manualPushMode = try container.liveDecodeLossyInt(forKey: .manualPushMode) ?? 1 manualPushState = try container.liveDecodeLossyInt(forKey: .manualPushState) ?? 0 viewsCount = try container.liveDecodeLossyInt(forKey: .viewsCount) ?? 0 } var displayStatus: String { statusLabel.liveNonEmpty ?? "状态\(status)" } var playbackURLCandidates: [String] { [playUrl, pullUrl, hlsUrl, liveUrl, flvUrl] } } /// 创建直播请求实体。 struct LiveCreateRequest: Encodable, Equatable { let scenicId: String let title: String let coverImg: String enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case title case coverImg = "cover_img" } } /// 直播控制请求实体。 struct LiveControlRequest: Encodable, Equatable { let liveId: Int enum CodingKeys: String, CodingKey { case liveId = "live_id" } } /// 直播推流模式切换请求实体。 struct LivePushModeRequest: Encodable, Equatable { let liveId: Int let manualPushMode: Int enum CodingKeys: String, CodingKey { case liveId = "live_id" case manualPushMode = "manual_push_mode" } } /// 直播相册列表响应实体。 struct LiveAlbumFolderListResponse: Decodable { let items: [LiveAlbumFolderItem] let page: Int let pageSize: Int let total: Int let totalPages: Int enum CodingKeys: String, CodingKey { case items case page case pageSize = "page_size" case total case totalPages = "total_pages" } init(items: [LiveAlbumFolderItem] = [], page: Int = 1, pageSize: Int = 10, total: Int = 0, totalPages: Int = 0) { self.items = items self.page = page self.pageSize = pageSize self.total = total self.totalPages = totalPages } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) items = (try? container.decode([LiveAlbumFolderItem].self, forKey: .items)) ?? [] page = try container.liveDecodeLossyInt(forKey: .page) ?? 1 pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10 total = try container.liveDecodeLossyInt(forKey: .total) ?? 0 totalPages = try container.liveDecodeLossyInt(forKey: .totalPages) ?? 0 } } /// 直播相册文件夹实体。 struct LiveAlbumFolderItem: Decodable, Identifiable, Equatable { let id: Int let albumId: Int let name: String let creator: String let items: [LiveAlbumFileItem] enum CodingKeys: String, CodingKey { case id case albumId = "album_id" case name case creator case items } init(id: Int = 0, albumId: Int = 0, name: String = "", creator: String = "", items: [LiveAlbumFileItem] = []) { self.id = id self.albumId = albumId self.name = name self.creator = creator self.items = items } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.liveDecodeLossyInt(forKey: .id) ?? 0 albumId = try container.liveDecodeLossyInt(forKey: .albumId) ?? 0 name = try container.liveDecodeLossyString(forKey: .name) creator = try container.liveDecodeLossyString(forKey: .creator) items = (try? container.decode([LiveAlbumFileItem].self, forKey: .items)) ?? [] } } /// 直播相册素材实体。 struct LiveAlbumFileItem: Decodable, Identifiable, Hashable { let id: Int let url: String let type: Int let size: Int64 let coverImg: String? enum CodingKeys: String, CodingKey { case id case url case type case size case coverImg = "cover_img" } init(id: Int = 0, url: String = "", type: Int = 1, size: Int64 = 0, coverImg: String? = nil) { self.id = id self.url = url self.type = type self.size = size self.coverImg = coverImg } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.liveDecodeLossyInt(forKey: .id) ?? 0 url = try container.liveDecodeLossyString(forKey: .url) type = try container.liveDecodeLossyInt(forKey: .type) ?? 1 size = Int64(try container.liveDecodeLossyInt(forKey: .size) ?? 0) coverImg = try? container.decodeIfPresent(String.self, forKey: .coverImg) } var isVideo: Bool { type == 2 } var previewURL: String { if let cover = coverImg?.liveTrimmed, !cover.isEmpty { return cover } return url } } /// 创建直播相册请求实体。 struct LiveAlbumCreateFolderRequest: Encodable, Equatable { let scenicId: String let name: String let items: [LiveAlbumCreateFileItem] enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case name case items } } /// 创建直播相册素材请求实体。 struct LiveAlbumCreateFileItem: Encodable, Equatable { let url: String let type: Int let size: Int64 let coverImg: String? enum CodingKeys: String, CodingKey { case url case type case size case coverImg = "cover_img" } } /// 删除直播相册请求实体。 struct LiveAlbumDeleteFolderRequest: Encodable, Equatable { let folderId: Int enum CodingKeys: String, CodingKey { case folderId = "folder_id" } } /// 删除直播相册素材请求实体。 struct LiveAlbumDeleteFilesRequest: Encodable, Equatable { let folderId: Int let fileIds: [Int] enum CodingKeys: String, CodingKey { case folderId = "folder_id" case fileIds = "file_ids" } } /// 本地待上传直播相册素材实体。 struct LiveAlbumLocalUploadFile: Identifiable, Equatable { let id: UUID let data: Data let fileName: String let fileType: Int let size: Int64 var uploadedURL: String? init(id: UUID = UUID(), data: Data, fileName: String, fileType: Int, size: Int64? = nil, uploadedURL: String? = nil) { self.id = id self.data = data self.fileName = fileName self.fileType = fileType self.size = size ?? Int64(data.count) self.uploadedURL = uploadedURL } var isVideo: Bool { fileType == 2 } } private extension KeyedDecodingContainer { func liveDecodeLossyString(forKey key: Key) throws -> String { if let value = try? decodeIfPresent(String.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Int64.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Double.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? "1" : "0" } return "" } func liveDecodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int64.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(String.self, forKey: key) { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int(trimmed) { return intValue } if let doubleValue = Double(trimmed) { return Int(doubleValue) } } return nil } } extension String { /// 直播模块内部使用的去空白字符串。 var liveTrimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } /// 直播模块内部使用的非空字符串兜底。 var liveNonEmpty: String? { let value = liveTrimmed return value.isEmpty ? nil : value } }