76 lines
2.3 KiB
Swift
76 lines
2.3 KiB
Swift
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?
|
|
}
|