import Foundation public struct LyricLine: Hashable, Sendable, Identifiable { public let time: TimeInterval public let text: String public var id: TimeInterval { time } public init(time: TimeInterval, text: String) { self.time = time self.text = text } } public struct Lyrics: Hashable, Sendable { public let lines: [LyricLine] public let isSynced: Bool public init(lines: [LyricLine], isSynced: Bool) { self.lines = lines self.isSynced = isSynced } public init?(lrc: String) { var lines: [LyricLine] = [] var plain: [String] = [] for raw in lrc.split(whereSeparator: \.isNewline) { var rest = Substring(raw) var stamps: [TimeInterval] = [] while rest.first == "[", let close = rest.firstIndex(of: "]") { let tag = rest[rest.index(after: rest.startIndex) ..< close] rest = rest[rest.index(after: close)...] if let time = Self.time(tag) { stamps.append(time) } } let text = rest.trimmingCharacters(in: .whitespaces) if stamps.isEmpty { if !text.isEmpty { plain.append(text) } } else { lines += stamps.map { LyricLine(time: $0, text: text) } } } if !lines.isEmpty { self.init(lines: lines.sorted { $0.time < $1.time }, isSynced: true) } else if !plain.isEmpty { self.init(lines: plain.map { LyricLine(time: 0, text: $0) }, isSynced: false) } else { return nil } } public func index(at time: TimeInterval) -> Int? { guard isSynced else { return nil } var found: Int? for (index, line) in lines.enumerated() where line.time <= time { found = index } return found } private static func time(_ tag: Substring) -> TimeInterval? { let parts = tag.split(separator: ":") guard parts.count == 2, let minutes = Double(parts[0]), let seconds = Double(parts[1]) else { return nil } return minutes * 60 + seconds } } public protocol LyricsSource: LibrarySource { func lyrics(for track: EntityID) async throws -> Lyrics? }