import Foundation /// M3U 播放列表解析器(支持 IPTV EXTINF 格式) enum M3UParser { struct Channel { var name: String var url: String var group: String? var logoURL: String? var type: String // "stream" or "url" } /// 从字符串内容解析 M3U static func parse(_ content: String) -> [Channel] { let lines = content.components(separatedBy: .newlines) var channels: [Channel] = [] var pendingName: String? var pendingGroup: String? var pendingLogo: String? for rawLine in lines { let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines) guard !line.isEmpty else { continue } // 跳过 M3U 头部 if line.hasPrefix("#EXTM3U") { continue } // 跳过其他注释(非 EXTINF) if line.hasPrefix("#") && !line.hasPrefix("#EXTINF") { continue } if line.hasPrefix("#EXTINF") { // 解析 #EXTINF:-1 tvg-name="..." group-title="..." tvg-logo="...",Channel Name let info = String(line.dropFirst("#EXTINF".count)) // 提取逗号后的频道名 if let commaIdx = info.lastIndex(of: ",") { let afterComma = String(info[info.index(after: commaIdx)...]) pendingName = afterComma.trimmingCharacters(in: .whitespaces) } // 提取属性 let attrs = parseAttributes(info) if let tvgName = attrs["tvg-name"], (pendingName ?? "").isEmpty { pendingName = tvgName } pendingGroup = attrs["group-title"] pendingLogo = attrs["tvg-logo"] } else if !line.hasPrefix("#") { // URL 行 let url = line let name = (pendingName ?? "").isEmpty ? (URL(string: url)?.host ?? url) : pendingName! let isStream = url.contains(".m3u8") || url.contains(".m3u") || url.contains("://") && !url.hasSuffix(".mp4") channels.append(Channel( name: name, url: url, group: pendingGroup, logoURL: pendingLogo, type: isStream ? "stream" : "url" )) pendingName = nil pendingGroup = nil pendingLogo = nil } } return channels } /// 从 URL 异步下载并解析 M3U static func fetchAndParse(url: URL) async throws -> [Channel] { let (data, _) = try await URLSession.shared.data(from: url) guard let content = String(data: data, encoding: .utf8) else { throw M3UError.invalidEncoding } return parse(content) } // MARK: - 私有 private static func parseAttributes(_ line: String) -> [String: String] { var attrs: [String: String] = [:] // 匹配 key="value" let pattern = #"([a-zA-Z\-]+)="([^"]*)""# guard let regex = try? NSRegularExpression(pattern: pattern) else { return attrs } let matches = regex.matches(in: line, range: NSRange(line.startIndex..., in: line)) for m in matches { guard m.numberOfRanges == 3, let keyRange = Range(m.range(at: 1), in: line), let valRange = Range(m.range(at: 2), in: line) else { continue } attrs[String(line[keyRange])] = String(line[valRange]) } return attrs } enum M3UError: Error, LocalizedError { case invalidEncoding case invalidURL case emptyResult var errorDescription: String? { switch self { case .invalidEncoding: return "无法解码M3U文件(需要UTF-8编码)" case .invalidURL: return "无效的URL" case .emptyResult: return "M3U文件中未找到频道" } } } }