first commit

This commit is contained in:
JinJiangHuang
2026-03-01 14:04:44 +08:00
commit 61e97da938
62 changed files with 9947 additions and 0 deletions
+216
View File
@@ -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]?
}
}
+72
View File
@@ -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
}
+82
View File
@@ -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)) ?? ""
}
}
}
+36
View File
@@ -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 = "" //
}
}
}
+133
View File
@@ -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)
}
}
}
+52
View File
@@ -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://")
}
}
+85
View File
@@ -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]
}
}