import Foundation import Combine /// 媒体库条目 struct LibraryItem: Identifiable, Codable, Equatable { let id: String var url: String var name: String var type: String // "file", "url", "stream", "local" var lastPosition: Double // 秒,上次播放位置 var liked: Bool var disliked: Bool var tags: [String] var addedAt: Date var group: String? // M3U group-title var logoURL: String? // M3U tvg-logo init(id: String = UUID().uuidString, url: String, name: String, type: String = "url", lastPosition: Double = 0, liked: Bool = false, disliked: Bool = false, tags: [String] = [], group: String? = nil, logoURL: String? = nil) { self.id = id self.url = url self.name = name self.type = type self.lastPosition = lastPosition self.liked = liked self.disliked = disliked self.tags = tags self.addedAt = Date() self.group = group self.logoURL = logoURL } static func == (lhs: LibraryItem, rhs: LibraryItem) -> Bool { lhs.id == rhs.id } } /// 播放列表 struct LibraryPlaylist: Identifiable, Codable, Equatable { let id: String var name: String var itemIDs: [String] var createdAt: Date init(id: String = UUID().uuidString, name: String, itemIDs: [String] = []) { self.id = id self.name = name self.itemIDs = itemIDs self.createdAt = Date() } static func == (lhs: LibraryPlaylist, rhs: LibraryPlaylist) -> Bool { lhs.id == rhs.id } } /// 持久化结构 private struct LibraryData: Codable { var items: [LibraryItem] var playlists: [LibraryPlaylist] } /// 媒体库管理器 — JSON文件持久化 @MainActor final class MediaLibrary: ObservableObject { @Published var items: [LibraryItem] = [] @Published var playlists: [LibraryPlaylist] = [] private let fileURL: URL init() { let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let dir = appSupport.appendingPathComponent("MiniPlayer", isDirectory: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) self.fileURL = dir.appendingPathComponent("library.json") load() } // MARK: - 持久化 private func load() { guard let data = try? Data(contentsOf: fileURL) else { return } guard let decoded = try? JSONDecoder().decode(LibraryData.self, from: data) else { return } items = decoded.items playlists = decoded.playlists } func save() { let data = LibraryData(items: items, playlists: playlists) guard let encoded = try? JSONEncoder().encode(data) else { return } try? encoded.write(to: fileURL, options: .atomic) } // MARK: - 条目操作 /// 添加条目,URL去重,返回已有或新建的item @discardableResult func addItem(url: String, name: String, type: String = "url", group: String? = nil, logoURL: String? = nil) -> LibraryItem { if let existing = items.first(where: { $0.url == url }) { return existing } let item = LibraryItem(url: url, name: name, type: type, group: group, logoURL: logoURL) items.append(item) save() return item } /// 批量添加(M3U导入),跳过已存在的URL func addItems(_ newItems: [LibraryItem]) -> Int { var count = 0 let existingURLs = Set(items.map { $0.url }) for item in newItems { if !existingURLs.contains(item.url) { items.append(item) count += 1 } } if count > 0 { save() } return count } func removeItem(_ id: String) { items.removeAll { $0.id == id } for i in playlists.indices { playlists[i].itemIDs.removeAll { $0 == id } } save() } // MARK: - 喜欢 / 不喜欢 func toggleLike(_ id: String) { guard let idx = items.firstIndex(where: { $0.id == id }) else { return } items[idx].liked.toggle() if items[idx].liked { items[idx].disliked = false } save() } func toggleDislike(_ id: String) { guard let idx = items.firstIndex(where: { $0.id == id }) else { return } items[idx].disliked.toggle() if items[idx].disliked { items[idx].liked = false } save() } // MARK: - 标签 func addTag(_ tag: String, to id: String) { guard let idx = items.firstIndex(where: { $0.id == id }) else { return } let t = tag.trimmingCharacters(in: .whitespaces) guard !t.isEmpty, !items[idx].tags.contains(t) else { return } items[idx].tags.append(t) items[idx].tags.sort() save() } func removeTag(_ tag: String, from id: String) { guard let idx = items.firstIndex(where: { $0.id == id }) else { return } items[idx].tags.removeAll { $0 == tag } save() } /// 所有已使用的标签(去重+排序) var allTags: [String] { Array(Set(items.flatMap { $0.tags })).sorted() } // MARK: - 播放位置 func savePosition(_ itemID: String, position: Double) { guard let idx = items.firstIndex(where: { $0.id == itemID }) else { return } items[idx].lastPosition = position save() } func getPosition(for itemID: String) -> Double { items.first(where: { $0.id == itemID })?.lastPosition ?? 0 } /// 按URL查找库条目 func findItem(byURL url: String) -> LibraryItem? { items.first { $0.url == url } } // MARK: - 播放列表 func createPlaylist(name: String) -> LibraryPlaylist { let pl = LibraryPlaylist(name: name) playlists.append(pl) save() return pl } func deletePlaylist(_ id: String) { playlists.removeAll { $0.id == id } save() } func renamePlaylist(_ id: String, name: String) { guard let idx = playlists.firstIndex(where: { $0.id == id }) else { return } playlists[idx].name = name save() } func addToPlaylist(_ playlistID: String, itemID: String) { guard let idx = playlists.firstIndex(where: { $0.id == playlistID }) else { return } guard !playlists[idx].itemIDs.contains(itemID) else { return } playlists[idx].itemIDs.append(itemID) save() } func removeFromPlaylist(_ playlistID: String, itemID: String) { guard let idx = playlists.firstIndex(where: { $0.id == playlistID }) else { return } playlists[idx].itemIDs.removeAll { $0 == itemID } save() } func items(in playlistID: String) -> [LibraryItem] { guard let pl = playlists.first(where: { $0.id == playlistID }) else { return [] } return pl.itemIDs.compactMap { id in items.first { $0.id == id } } } // MARK: - 搜索(模糊查询) func search(_ text: String, tag: String? = nil, likedOnly: Bool = false) -> [LibraryItem] { items.filter { item in // 喜欢过滤 if likedOnly && !item.liked { return false } // 标签过滤 if let tag = tag, !item.tags.contains(tag) { return false } // 模糊搜索 guard !text.isEmpty else { return true } let q = text.lowercased() return item.name.lowercased().contains(q) || item.url.lowercased().contains(q) || item.tags.contains(where: { $0.lowercased().contains(q) }) || (item.group?.lowercased().contains(q) ?? false) } } // MARK: - 导出 /// 库条目总数 var count: Int { items.count } }