|
| 1 | +import Foundation |
| 2 | + |
| 3 | +struct LRCLIBRepsonse: Decodable { |
| 4 | + struct LyricLine: Decodable, Hashable { |
| 5 | + let startTimeMS: TimeInterval |
| 6 | + let words: String |
| 7 | + let id = UUID() |
| 8 | + |
| 9 | + enum CodingKeys: String, CodingKey { |
| 10 | + case startTimeMS = "startTimeMs" |
| 11 | + case words |
| 12 | + } |
| 13 | + |
| 14 | + init(from decoder: Decoder) throws { |
| 15 | + let container = try decoder.container(keyedBy: CodingKeys.self) |
| 16 | + self.startTimeMS = try TimeInterval(container.decode(String.self, forKey: .startTimeMS))! |
| 17 | + self.words = try container.decode(String.self, forKey: .words) |
| 18 | + } |
| 19 | + |
| 20 | + init(startTime: TimeInterval, words: String) { |
| 21 | + self.startTimeMS = startTime |
| 22 | + self.words = words |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + let id: Int |
| 27 | + let name, trackName, artistName, albumName: String |
| 28 | + let duration: Int |
| 29 | + let instrumental: Bool |
| 30 | + let plainLyrics, syncedLyrics: String |
| 31 | + let lyrics: [LyricLine] |
| 32 | + |
| 33 | + enum CodingKeys: CodingKey { |
| 34 | + case id |
| 35 | + case name |
| 36 | + case trackName |
| 37 | + case artistName |
| 38 | + case albumName |
| 39 | + case duration |
| 40 | + case instrumental |
| 41 | + case plainLyrics |
| 42 | + case syncedLyrics |
| 43 | +// case lyrics |
| 44 | + } |
| 45 | + |
| 46 | + static func decodeLyrics(input: String) -> [LyricLine] { |
| 47 | + var lyricsArray: [LyricLine] = [] |
| 48 | + let lines = input.components(separatedBy: "\n") |
| 49 | + |
| 50 | + for line in lines { |
| 51 | + // Use regex to match the timestamp and the lyrics |
| 52 | + let regex = try! NSRegularExpression(pattern: #"\[(\d{2}:\d{2}\.\d{2})\]\s*(.*)"#) |
| 53 | + let matches = regex.matches(in: line, range: NSRange(line.startIndex ..< line.endIndex, in: line)) |
| 54 | + |
| 55 | + for match in matches { |
| 56 | + if let timestampRange = Range(match.range(at: 1), in: line), |
| 57 | + let lyricsRange = Range(match.range(at: 2), in: line) { |
| 58 | + let timestamp = String(line[timestampRange]) |
| 59 | + let lyrics = String(line[lyricsRange]) |
| 60 | + lyricsArray.append(LyricLine(startTime: timestamp.convertToTimeInterval(), words: lyrics)) |
| 61 | + } |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + return lyricsArray |
| 66 | + } |
| 67 | + |
| 68 | + init(from decoder: any Decoder) throws { |
| 69 | + let container = try decoder.container(keyedBy: CodingKeys.self) |
| 70 | + self.id = try container.decode(Int.self, forKey: .id) |
| 71 | + self.name = try container.decode(String.self, forKey: .name) |
| 72 | + self.trackName = try container.decode(String.self, forKey: .trackName) |
| 73 | + self.artistName = try container.decode(String.self, forKey: .artistName) |
| 74 | + self.albumName = try container.decode(String.self, forKey: .albumName) |
| 75 | + self.duration = try container.decode(Int.self, forKey: .duration) |
| 76 | + self.instrumental = try container.decode(Bool.self, forKey: .instrumental) |
| 77 | + if instrumental { |
| 78 | + self.plainLyrics = "" |
| 79 | + self.syncedLyrics = "" |
| 80 | + self.lyrics = [] |
| 81 | + } else { |
| 82 | + self.plainLyrics = try container.decode(String.self, forKey: .plainLyrics) |
| 83 | + self.syncedLyrics = try container.decode(String.self, forKey: .syncedLyrics) |
| 84 | + self.lyrics = Self.decodeLyrics(input: syncedLyrics) |
| 85 | + } |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +extension String { |
| 90 | + fileprivate func convertToTimeInterval() -> TimeInterval { |
| 91 | + guard self != "" else { |
| 92 | + return 0 |
| 93 | + } |
| 94 | + |
| 95 | + var interval: Double = 0 |
| 96 | + |
| 97 | + let parts = self.components(separatedBy: ":") |
| 98 | + for (index, part) in parts.reversed().enumerated() { |
| 99 | + interval += (Double(part) ?? 0) * pow(Double(60), Double(index)) |
| 100 | + } |
| 101 | + |
| 102 | + return interval * 1000 // Convert seconds to milliseconds |
| 103 | + } |
| 104 | +} |
0 commit comments