MiniPlayer/Sources/M3UParser.swift
yumoqing 05afadfd11 feat: 媒体库 - 播放记忆、多播放列表、M3U导入、模糊搜索、喜欢/标签系统
- MediaLibrary.swift: 数据模型(JSON持久化) + 播放列表管理 + 标签/喜欢
- M3UParser.swift: 解析IPTV格式M3U播放列表(EXTINF属性)
- MediaLibraryView.swift: NavigationSplitView侧边栏+内容区UI
- PlayerBridge.swift: 播放位置记忆(暂停/切歌/退出保存,播放恢复)
- 本地视频自动入库,搜索结果临时播放列表,所有操作不中断播放
- 快捷键: Cmd+Shift+L 媒体库, Cmd+Shift+I 导入M3U
2026-06-30 00:28:14 +08:00

118 lines
4.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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文件中未找到频道"
}
}
}
}