first commit
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - 灵活类型解码器
|
||||
|
||||
/// 兼容 JSON 中数值字段可能是字符串 "0" 或整数 0 的情况
|
||||
/// Android Gson 自动处理此差异,Swift 需要手动兼容
|
||||
struct FlexibleInt: Codable, Hashable {
|
||||
let value: Int
|
||||
|
||||
init(_ value: Int) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let intVal = try? container.decode(Int.self) {
|
||||
self.value = intVal
|
||||
} else if let stringVal = try? container.decode(String.self),
|
||||
let intVal = Int(stringVal) {
|
||||
self.value = intVal
|
||||
} else {
|
||||
self.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容 ext 字段可能是字符串或对象
|
||||
struct FlexibleExt: Codable, Hashable {
|
||||
let stringValue: String?
|
||||
let dictValue: [String: AnyCodableValue]?
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let str = try? container.decode(String.self) {
|
||||
self.stringValue = str
|
||||
self.dictValue = nil
|
||||
} else if let dict = try? container.decode([String: AnyCodableValue].self) {
|
||||
self.stringValue = nil
|
||||
self.dictValue = dict
|
||||
} else {
|
||||
self.stringValue = nil
|
||||
self.dictValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
if let str = stringValue {
|
||||
try container.encode(str)
|
||||
} else if let dict = dictValue {
|
||||
try container.encode(dict)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用的 JSON 值类型,用于处理混合类型的字典
|
||||
enum AnyCodableValue: Codable, Hashable {
|
||||
case string(String)
|
||||
case int(Int)
|
||||
case double(Double)
|
||||
case bool(Bool)
|
||||
case dict([String: AnyCodableValue])
|
||||
case array([AnyCodableValue])
|
||||
case null
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let str = try? container.decode(String.self) {
|
||||
self = .string(str)
|
||||
} else if let int = try? container.decode(Int.self) {
|
||||
self = .int(int)
|
||||
} else if let double = try? container.decode(Double.self) {
|
||||
self = .double(double)
|
||||
} else if let bool = try? container.decode(Bool.self) {
|
||||
self = .bool(bool)
|
||||
} else if let dict = try? container.decode([String: AnyCodableValue].self) {
|
||||
self = .dict(dict)
|
||||
} else if let array = try? container.decode([AnyCodableValue].self) {
|
||||
self = .array(array)
|
||||
} else {
|
||||
self = .null
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .string(let s): try container.encode(s)
|
||||
case .int(let i): try container.encode(i)
|
||||
case .double(let d): try container.encode(d)
|
||||
case .bool(let b): try container.encode(b)
|
||||
case .dict(let d): try container.encode(d)
|
||||
case .array(let a): try container.encode(a)
|
||||
case .null: try container.encodeNil()
|
||||
}
|
||||
}
|
||||
|
||||
/// 将可能的任意类型转换为字符串(如果原本是字典或数组,则转为 JSON 字符串)
|
||||
var stringValue: String? {
|
||||
switch self {
|
||||
case .string(let s): return s
|
||||
case .int(let i): return String(i)
|
||||
case .double(let d): return String(d)
|
||||
case .bool(let b): return b ? "true" : "false"
|
||||
case .null: return nil
|
||||
case .dict, .array:
|
||||
if let data = try? JSONEncoder().encode(self),
|
||||
let str = String(data: data, encoding: .utf8) {
|
||||
return str
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 解析接口配置
|
||||
|
||||
/// 解析接口配置 - 对应 Android 版 ParseBean.java
|
||||
struct ParseBean: Codable, Identifiable, Hashable {
|
||||
var id: String { name }
|
||||
var name: String = ""
|
||||
var url: String = ""
|
||||
var type: Int = 0 // 0:嗅探 1:解析
|
||||
var ext: [String: String]? = nil
|
||||
|
||||
init(name: String = "", url: String = "", type: Int = 0) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API 配置顶层结构
|
||||
|
||||
/// API 配置顶层结构 - 对应 JSON 配置文件格式
|
||||
struct AppConfigData: Codable {
|
||||
var spider: String?
|
||||
var wallpaper: String?
|
||||
var sites: [SiteConfig]?
|
||||
var parses: [ParseConfig]?
|
||||
var lives: [LiveConfig]?
|
||||
var doh: [DoHConfig]?
|
||||
var rules: [RuleConfig]?
|
||||
var hosts: [String]?
|
||||
var flags: [String]?
|
||||
var ads: [String]?
|
||||
|
||||
struct SiteConfig: Codable {
|
||||
var key: String?
|
||||
var name: String?
|
||||
var api: String?
|
||||
var searchable: FlexibleInt?
|
||||
var filterable: FlexibleInt?
|
||||
var quickSearch: FlexibleInt?
|
||||
var playerType: FlexibleInt?
|
||||
var type: FlexibleInt?
|
||||
var ext: AnyCodableValue?
|
||||
var jar: String?
|
||||
var style: AnyCodableValue?
|
||||
var playUrl: String?
|
||||
var categories: [String]?
|
||||
var click: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case key, name, api, searchable, filterable, quickSearch
|
||||
case playerType, type, ext, jar, style, playUrl, categories, click
|
||||
// type_flag 也可能出现
|
||||
}
|
||||
}
|
||||
|
||||
struct ParseConfig: Codable {
|
||||
var name: String?
|
||||
var url: String?
|
||||
var type: FlexibleInt?
|
||||
var ext: FlexibleExt?
|
||||
}
|
||||
|
||||
struct LiveConfig: Codable {
|
||||
var name: String?
|
||||
var url: String?
|
||||
var type: FlexibleInt?
|
||||
var ua: String?
|
||||
var epg: String?
|
||||
var logo: String?
|
||||
var pass: AnyCodableValue? // Android 配置中存在
|
||||
var playerType: FlexibleInt?
|
||||
var channels: [LiveChannelConfig]?
|
||||
|
||||
struct LiveChannelConfig: Codable {
|
||||
var name: String?
|
||||
var urls: [String]?
|
||||
var group: String?
|
||||
var logo: String?
|
||||
}
|
||||
}
|
||||
|
||||
struct DoHConfig: Codable {
|
||||
var name: String?
|
||||
var url: String?
|
||||
}
|
||||
|
||||
struct RuleConfig: Codable {
|
||||
var name: String?
|
||||
var host: String?
|
||||
var hosts: [String]?
|
||||
var rule: [String]?
|
||||
var regex: [String]?
|
||||
var filter: [String]?
|
||||
var script: [String]?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
|
||||
/// 直播相关模型 - 对应 Android 版 LiveChannel*.java / Epginfo.java
|
||||
|
||||
/// 直播频道分组
|
||||
struct LiveChannelGroup: Codable, Identifiable, Hashable {
|
||||
var id: String { groupName }
|
||||
var groupName: String = ""
|
||||
var groupIndex: Int = 0
|
||||
var channels: [LiveChannelItem] = []
|
||||
var isPassword: Bool = false
|
||||
|
||||
init(groupName: String = "", groupIndex: Int = 0) {
|
||||
self.groupName = groupName
|
||||
self.groupIndex = groupIndex
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播频道
|
||||
struct LiveChannelItem: Codable, Identifiable, Hashable {
|
||||
var id: String { "\(channelName)_\(channelIndex)" }
|
||||
var channelName: String = ""
|
||||
var channelIndex: Int = 0
|
||||
var channelUrls: [String] = []
|
||||
var sourceIndex: Int = 0
|
||||
var sourceNum: Int { channelUrls.count }
|
||||
var logo: String = ""
|
||||
|
||||
init(channelName: String = "", channelIndex: Int = 0) {
|
||||
self.channelName = channelName
|
||||
self.channelIndex = channelIndex
|
||||
}
|
||||
|
||||
var currentUrl: String? {
|
||||
guard sourceIndex >= 0, sourceIndex < channelUrls.count else { return channelUrls.first }
|
||||
return channelUrls[sourceIndex]
|
||||
}
|
||||
|
||||
mutating func nextSource() {
|
||||
if channelUrls.count > 0 {
|
||||
sourceIndex = (sourceIndex + 1) % channelUrls.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EPG 节目信息
|
||||
struct Epginfo: Codable, Identifiable, Hashable {
|
||||
var id: String { "\(title)_\(startTime)" }
|
||||
var title: String = ""
|
||||
var startTime: String = ""
|
||||
var endTime: String = ""
|
||||
var index: Int = 0
|
||||
|
||||
var isLive: Bool {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
guard let start = formatter.date(from: startTime),
|
||||
let end = formatter.date(from: endTime) else { return false }
|
||||
|
||||
let now = formatter.date(from: formatter.string(from: Date()))!
|
||||
return now >= start && now < end
|
||||
}
|
||||
}
|
||||
|
||||
/// EPG 日期分组
|
||||
struct LiveEpgDate: Codable, Identifiable, Hashable {
|
||||
var id: String { datePresent }
|
||||
var datePresent: String = "" // 显示日期
|
||||
var date: String = "" // 查询日期
|
||||
var index: Int = 0
|
||||
var isSelected: Bool = false
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
|
||||
/// 电影/视频数据模型 - 对应 Android 版 Movie.java
|
||||
struct Movie: Codable {
|
||||
var videoList: [Video] = []
|
||||
var pagecount: Int = 0
|
||||
var page: Int = 0
|
||||
var total: Int = 0
|
||||
var limit: Int = 0
|
||||
|
||||
/// 单个视频条目
|
||||
struct Video: Codable, Identifiable, Hashable {
|
||||
var id: String
|
||||
var name: String = ""
|
||||
var pic: String = ""
|
||||
var note: String = "" // 备注(如"更新至第20集")
|
||||
var year: String = ""
|
||||
var area: String = ""
|
||||
var type: String = "" // 类型/分类名
|
||||
var director: String = ""
|
||||
var actor: String = ""
|
||||
var des: String = "" // 简介
|
||||
var sourceKey: String = "" // 来源站点 key
|
||||
var tid: String = "" // 分类 ID
|
||||
var last: String = "" // 最后更新
|
||||
var dt: String = "" // 日期
|
||||
|
||||
init(id: String = UUID().uuidString, name: String = "", pic: String = "",
|
||||
note: String = "", sourceKey: String = "") {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.pic = pic
|
||||
self.note = note
|
||||
self.sourceKey = sourceKey
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "vod_id"
|
||||
case name = "vod_name"
|
||||
case pic = "vod_pic"
|
||||
case note = "vod_remarks"
|
||||
case year = "vod_year"
|
||||
case area = "vod_area"
|
||||
case type = "type_name"
|
||||
case director = "vod_director"
|
||||
case actor = "vod_actor"
|
||||
case des = "vod_content"
|
||||
case tid = "type_id"
|
||||
case last = "vod_time"
|
||||
case dt = "vod_play_from"
|
||||
case sourceKey
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
// 支持 String 或 Int 类型的 id
|
||||
if let intId = try? container.decode(Int.self, forKey: .id) {
|
||||
self.id = String(intId)
|
||||
} else {
|
||||
self.id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
|
||||
}
|
||||
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
|
||||
self.pic = (try? container.decode(String.self, forKey: .pic)) ?? ""
|
||||
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
|
||||
self.year = (try? container.decode(String.self, forKey: .year)) ?? ""
|
||||
self.area = (try? container.decode(String.self, forKey: .area)) ?? ""
|
||||
self.type = (try? container.decode(String.self, forKey: .type)) ?? ""
|
||||
self.director = (try? container.decode(String.self, forKey: .director)) ?? ""
|
||||
self.actor = (try? container.decode(String.self, forKey: .actor)) ?? ""
|
||||
self.des = (try? container.decode(String.self, forKey: .des)) ?? ""
|
||||
self.tid = {
|
||||
if let intTid = try? container.decode(Int.self, forKey: .tid) {
|
||||
return String(intTid)
|
||||
}
|
||||
return (try? container.decode(String.self, forKey: .tid)) ?? ""
|
||||
}()
|
||||
self.last = (try? container.decode(String.self, forKey: .last)) ?? ""
|
||||
self.dt = (try? container.decode(String.self, forKey: .dt)) ?? ""
|
||||
self.sourceKey = (try? container.decode(String.self, forKey: .sourceKey)) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
/// 分类排序模型 - 对应 Android 版 MovieSort.java
|
||||
struct MovieSort: Codable {
|
||||
var sortList: [SortData] = []
|
||||
|
||||
/// 单个分类数据
|
||||
struct SortData: Codable, Identifiable, Hashable {
|
||||
var id: String
|
||||
var name: String = ""
|
||||
var flag: String = ""
|
||||
var filters: [SortFilter] = []
|
||||
|
||||
init(id: String = "", name: String = "", flag: String = "") {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.flag = flag
|
||||
}
|
||||
|
||||
static func home() -> SortData {
|
||||
SortData(id: "home", name: "推荐", flag: "1")
|
||||
}
|
||||
}
|
||||
|
||||
/// 筛选条件
|
||||
struct SortFilter: Codable, Hashable {
|
||||
var key: String = ""
|
||||
var name: String = ""
|
||||
var values: [SortFilterValue] = []
|
||||
|
||||
struct SortFilterValue: Codable, Hashable {
|
||||
var n: String = "" // 显示名
|
||||
var v: String = "" // 值
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
|
||||
/// 播放器引擎类型
|
||||
enum PlayerEngine: Int, CaseIterable, Identifiable {
|
||||
case system = 0
|
||||
case vlc = 10
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .system:
|
||||
return "系统播放器"
|
||||
case .vlc:
|
||||
return "VLC播放器"
|
||||
}
|
||||
}
|
||||
|
||||
static var isVLCAvailable: Bool {
|
||||
#if canImport(VLCKitSPM)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var availableEngines: [PlayerEngine] {
|
||||
var engines: [PlayerEngine] = [.system]
|
||||
if isVLCAvailable {
|
||||
engines.append(.vlc)
|
||||
}
|
||||
return engines
|
||||
}
|
||||
|
||||
static func fromStoredValue(_ rawValue: Int) -> PlayerEngine {
|
||||
guard let engine = PlayerEngine(rawValue: rawValue) else {
|
||||
return .system
|
||||
}
|
||||
|
||||
if engine == .vlc && !isVLCAvailable {
|
||||
return .system
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频解码模式
|
||||
enum VideoDecodeMode: Int, CaseIterable, Identifiable {
|
||||
case auto = 0
|
||||
case hardware = 1
|
||||
case software = 2
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .auto:
|
||||
return "自动"
|
||||
case .hardware:
|
||||
return "硬解码"
|
||||
case .software:
|
||||
return "软解码"
|
||||
}
|
||||
}
|
||||
|
||||
static func fromStoredValue(_ rawValue: Int) -> VideoDecodeMode {
|
||||
VideoDecodeMode(rawValue: rawValue) ?? .auto
|
||||
}
|
||||
|
||||
/// VLC 媒体选项
|
||||
var vlcHardwareDecodeOption: String? {
|
||||
switch self {
|
||||
case .auto:
|
||||
// 自动模式默认硬解优先,异常时用户仍可手动切到软解。
|
||||
return "any"
|
||||
case .hardware:
|
||||
return "any"
|
||||
case .software:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// VLC 缓冲策略
|
||||
enum VLCBufferMode: Int, CaseIterable, Identifiable {
|
||||
case lowLatency = 0
|
||||
case balanced = 1
|
||||
case smooth = 2
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .lowLatency:
|
||||
return "低延迟"
|
||||
case .balanced:
|
||||
return "均衡"
|
||||
case .smooth:
|
||||
return "流畅优先"
|
||||
}
|
||||
}
|
||||
|
||||
static var defaultMode: VLCBufferMode { .balanced }
|
||||
|
||||
static func fromStoredValue(_ rawValue: Int) -> VLCBufferMode {
|
||||
VLCBufferMode(rawValue: rawValue) ?? defaultMode
|
||||
}
|
||||
|
||||
var enableFrameDrop: Bool {
|
||||
self == .lowLatency
|
||||
}
|
||||
|
||||
func cacheConfig(isLive: Bool) -> (network: Int, live: Int, file: Int) {
|
||||
switch self {
|
||||
case .lowLatency:
|
||||
if isLive {
|
||||
return (network: 1200, live: 1200, file: 1600)
|
||||
}
|
||||
return (network: 1800, live: 1600, file: 2400)
|
||||
case .balanced:
|
||||
if isLive {
|
||||
return (network: 2600, live: 2600, file: 3200)
|
||||
}
|
||||
return (network: 3800, live: 3200, file: 4400)
|
||||
case .smooth:
|
||||
if isLive {
|
||||
return (network: 4200, live: 4200, file: 5200)
|
||||
}
|
||||
return (network: 5600, live: 4800, file: 6400)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
|
||||
/// 视频源站点配置 - 对应 Android 版 SourceBean.java
|
||||
struct SourceBean: Codable, Identifiable, Hashable {
|
||||
var id: String { key }
|
||||
|
||||
let key: String
|
||||
let name: String
|
||||
let api: String
|
||||
let searchable: Int // 0:关闭搜索 1:启用搜索
|
||||
let filterable: Int // 0:首页不可选 1:首页可选
|
||||
let playerType: Int // 0:系统 1:IJK 2:EXO
|
||||
let type: Int // 0:xml 1:json 3:jar 4:remote
|
||||
let ext: String?
|
||||
|
||||
init(key: String = "", name: String = "", api: String = "",
|
||||
searchable: Int = 1, filterable: Int = 1,
|
||||
playerType: Int = 0, type: Int = 1, ext: String? = nil) {
|
||||
self.key = key
|
||||
self.name = name
|
||||
self.api = api
|
||||
self.searchable = searchable
|
||||
self.filterable = filterable
|
||||
self.playerType = playerType
|
||||
self.type = type
|
||||
self.ext = ext
|
||||
}
|
||||
|
||||
var isSearchable: Bool { searchable == 1 }
|
||||
var isFilterable: Bool { filterable == 1 }
|
||||
|
||||
/// 是否在 Swift 版中受支持(type=3 为 JAR/Spider,需要 Java 运行时,暂不支持)
|
||||
var isSupportedInSwift: Bool {
|
||||
return type == 0 || type == 1 || type == 4
|
||||
}
|
||||
|
||||
/// 类型描述
|
||||
var typeDescription: String {
|
||||
switch type {
|
||||
case 0: return "XML"
|
||||
case 1: return "JSON"
|
||||
case 3: return "JAR"
|
||||
case 4: return "Remote"
|
||||
default: return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
/// api 字段是否为有效 HTTP URL
|
||||
var isHttpApi: Bool {
|
||||
return api.hasPrefix("http://") || api.hasPrefix("https://")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import Foundation
|
||||
|
||||
/// 视频详情模型 - 对应 Android 版 VodInfo.java
|
||||
struct VodInfo: Codable, Identifiable {
|
||||
var id: String
|
||||
var name: String = ""
|
||||
var pic: String = ""
|
||||
var note: String = ""
|
||||
var year: String = ""
|
||||
var area: String = ""
|
||||
var typeName: String = ""
|
||||
var director: String = ""
|
||||
var actor: String = ""
|
||||
var des: String = ""
|
||||
var sourceKey: String = ""
|
||||
|
||||
/// 播放来源(线路)列表
|
||||
var playFlags: [String] = []
|
||||
/// key: flag名称, value: 剧集列表
|
||||
var playUrlMap: [String: [Episode]] = [:]
|
||||
|
||||
var playFlag: String = "" // 当前选中线路
|
||||
var playIndex: Int = 0 // 当前播放剧集索引
|
||||
|
||||
/// 单集信息
|
||||
struct Episode: Codable, Identifiable, Hashable {
|
||||
var id: String { name }
|
||||
let name: String
|
||||
let url: String
|
||||
|
||||
init(name: String, url: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 Movie.Video 和详情数据构建
|
||||
static func from(video: Movie.Video, playFrom: String, playUrl: String) -> VodInfo {
|
||||
var info = VodInfo(id: video.id)
|
||||
info.name = video.name
|
||||
info.pic = video.pic
|
||||
info.note = video.note
|
||||
info.year = video.year
|
||||
info.area = video.area
|
||||
info.typeName = video.type
|
||||
info.director = video.director
|
||||
info.actor = video.actor
|
||||
info.des = video.des.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
|
||||
info.sourceKey = video.sourceKey
|
||||
|
||||
// 解析播放列表
|
||||
// playFrom 格式: "线路1$$$线路2$$$线路3"
|
||||
// playUrl 格式: "第1集$url1#第2集$url2$$$第1集$url3#第2集$url4"
|
||||
let flags = playFrom.components(separatedBy: "$$$").filter { !$0.isEmpty }
|
||||
let urls = playUrl.components(separatedBy: "$$$")
|
||||
|
||||
info.playFlags = flags
|
||||
for (i, flag) in flags.enumerated() {
|
||||
if i < urls.count {
|
||||
let episodes = urls[i].components(separatedBy: "#").compactMap { item -> Episode? in
|
||||
let parts = item.components(separatedBy: "$")
|
||||
guard parts.count >= 2 else { return nil }
|
||||
return Episode(name: parts[0], url: parts[1])
|
||||
}
|
||||
info.playUrlMap[flag] = episodes
|
||||
}
|
||||
}
|
||||
|
||||
if let first = flags.first {
|
||||
info.playFlag = first
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
var currentEpisodes: [Episode] {
|
||||
playUrlMap[playFlag] ?? []
|
||||
}
|
||||
|
||||
var currentEpisode: Episode? {
|
||||
let eps = currentEpisodes
|
||||
guard playIndex >= 0, playIndex < eps.count else { return nil }
|
||||
return eps[playIndex]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user