Skip to content

Commit bbc02fb

Browse files
committed
Fetch lyrics from online source when enabled
Adds support for fetching track lyrics from lrclib.net in case it is not available locally and user has the setting enabled to fetch online.
1 parent f8197e8 commit bbc02fb

5 files changed

Lines changed: 364 additions & 61 deletions

File tree

Core/LyricsLoader.swift

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,52 @@ import Foundation
22
import GRDB
33

44
struct LyricsLoader {
5-
/// Load lyrics for a track, checking external files first, then embedded lyrics
5+
/// Load lyrics for a track, checking external files first, then embedded lyrics, then online
66
/// - Parameters:
77
/// - track: The track to load lyrics for
88
/// - dbQueue: Database queue for fetching embedded lyrics
9+
/// - databaseManager: Database manager for online lyrics storage (optional)
910
/// - Returns: Tuple containing lyrics text and source type
1011
static func loadLyrics(
1112
for track: Track,
12-
using dbQueue: DatabaseQueue
13+
using dbQueue: DatabaseQueue,
14+
databaseManager: DatabaseManager? = nil
1315
) async throws -> (lyrics: String, source: LyricsSource) {
16+
var rawLyrics: String?
17+
var source: LyricsSource = .none
18+
1419
// First, check for external LRC/SRT files
1520
if let externalLyrics = try? loadExternalLyrics(for: track) {
16-
return externalLyrics
21+
rawLyrics = externalLyrics.lyrics
22+
source = externalLyrics.source
1723
}
1824

19-
// Fall back to embedded lyrics
20-
if let fullTrack = try? await track.fullTrack(using: dbQueue),
25+
// Second, check for embedded lyrics (stored in db during library scan)
26+
let fullTrack = try? await track.fullTrack(using: dbQueue)
27+
if rawLyrics == nil,
28+
let fullTrack = fullTrack,
2129
let embeddedLyrics = fullTrack.extendedMetadata?.lyrics,
2230
!embeddedLyrics.isEmpty {
23-
return (embeddedLyrics, .embedded)
31+
rawLyrics = embeddedLyrics
32+
source = .embedded
33+
}
34+
35+
// Finally, try fetching from online source
36+
if rawLyrics == nil,
37+
let fullTrack = fullTrack,
38+
let databaseManager = databaseManager,
39+
let onlineLyrics = await LyricsManager.shared.fetchLyrics(for: fullTrack, using: databaseManager) {
40+
rawLyrics = onlineLyrics
41+
source = .online
2442
}
2543

26-
// No lyrics found
27-
return ("", .none)
44+
// Strip timestamps for display
45+
guard let lyrics = rawLyrics else {
46+
return ("", .none)
47+
}
48+
49+
let displayLyrics = stripTimestamps(lyrics)
50+
return (displayLyrics, source)
2851
}
2952

3053
/// Check for and load external lyrics files (.lrc or .srt)
@@ -68,54 +91,87 @@ struct LyricsLoader {
6891
return nil
6992
}
7093

94+
// MARK: - Timestamp Stripping
95+
96+
/// Strip LRC-style timestamps from lyrics for display
97+
private static func stripTimestamps(_ content: String) -> String {
98+
let lines = content.components(separatedBy: .newlines)
99+
var strippedLines: [String] = []
100+
101+
for line in lines {
102+
var currentLine = line
103+
104+
// Remove all timestamp tags [mm:ss.xx] from the line
105+
while currentLine.hasPrefix("[") {
106+
if let endBracket = currentLine.firstIndex(of: "]") {
107+
let tag = String(currentLine[currentLine.index(after: currentLine.startIndex)..<endBracket])
108+
// Check if it's a timestamp (contains digits and colons/periods)
109+
let isTimestamp = tag.contains(":") && tag.rangeOfCharacter(from: .decimalDigits) != nil
110+
111+
if isTimestamp {
112+
currentLine = String(currentLine[currentLine.index(after: endBracket)...])
113+
} else {
114+
break
115+
}
116+
} else {
117+
break
118+
}
119+
}
120+
121+
let trimmed = currentLine.trimmingCharacters(in: .whitespaces)
122+
if !trimmed.isEmpty {
123+
strippedLines.append(trimmed)
124+
}
125+
}
126+
127+
return strippedLines.joined(separator: "\n")
128+
}
129+
130+
// MARK: - Format Parsing
131+
71132
/// Parse LRC file format and extract lyrics text
72133
private static func parseLRC(_ content: String) -> String {
73134
let lines = content.components(separatedBy: .newlines)
74135
var lyricsLines: [String] = []
75136

76137
for line in lines {
77-
// LRC format: [mm:ss.xx]lyrics text
78-
// Remove timestamp and metadata tags for now
79138
if line.hasPrefix("[") {
80139
if let endBracket = line.firstIndex(of: "]") {
81-
let afterBracket = line.index(after: endBracket)
82-
if afterBracket < line.endIndex {
83-
let lyricsText = String(line[afterBracket...]).trimmingCharacters(in: .whitespaces)
84-
if !lyricsText.isEmpty {
85-
// Skip metadata lines (ar:, ti:, al:, etc.)
86-
let tag = String(line[line.index(after: line.startIndex)..<endBracket])
87-
if !tag.contains(":") || tag.contains(".") {
88-
lyricsLines.append(lyricsText)
89-
}
90-
}
140+
let tag = String(line[line.index(after: line.startIndex)..<endBracket])
141+
142+
// Skip metadata lines (ar:, ti:, al:, etc.) but not timestamps
143+
let isMetadata = tag.contains(":") && tag.rangeOfCharacter(from: .decimalDigits) == nil
144+
if isMetadata {
145+
continue
91146
}
147+
148+
// Keep the full line (with timestamps) for now
149+
lyricsLines.append(line)
92150
}
93151
} else if !line.trimmingCharacters(in: .whitespaces).isEmpty {
94152
lyricsLines.append(line)
95153
}
96154
}
97155

98-
return lyricsLines.joined(separator: "\n")
156+
// Strip timestamps at the end
157+
return stripTimestamps(lyricsLines.joined(separator: "\n"))
99158
}
100159

101-
/// Parse SRT file format and extract lyrics text (ignoring timestamps for now)
160+
/// Parse SRT file format and extract lyrics text
102161
private static func parseSRT(_ content: String) -> String {
103162
let lines = content.components(separatedBy: .newlines)
104163
var lyricsLines: [String] = []
105-
var skipNext = false
106164

107165
for line in lines {
108166
let trimmed = line.trimmingCharacters(in: .whitespaces)
109167

110-
// Skip empty lines and sequence numbers
168+
// Skip empty lines
111169
if trimmed.isEmpty {
112-
skipNext = false
113170
continue
114171
}
115172

116173
// Skip timestamp lines (format: 00:00:00,000 --> 00:00:00,000)
117174
if trimmed.contains("-->") {
118-
skipNext = true
119175
continue
120176
}
121177

@@ -124,12 +180,6 @@ struct LyricsLoader {
124180
continue
125181
}
126182

127-
// Skip the line immediately after timestamp
128-
if skipNext {
129-
skipNext = false
130-
}
131-
132-
// This is lyrics text
133183
lyricsLines.append(trimmed)
134184
}
135185

@@ -141,5 +191,6 @@ enum LyricsSource {
141191
case lrc
142192
case srt
143193
case embedded
194+
case online
144195
case none
145196
}

Managers/Database/DMTrackUpdate.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension DatabaseManager {
3232
}
3333
}
3434

35-
// Batch update for track properties (more efficient for multiple updates)
35+
/// Batch update for track properties
3636
func updateTrack(_ track: Track) async throws {
3737
guard track.trackId != nil else {
3838
throw DatabaseError.invalidTrackId
@@ -42,4 +42,21 @@ extension DatabaseManager {
4242
try track.update(db)
4343
}
4444
}
45+
46+
/// Updates a track's lyrics in extended_metadata
47+
func updateTrackLyrics(for fullTrack: FullTrack, lyrics: String) async throws {
48+
guard fullTrack.trackId != nil else {
49+
throw DatabaseError.invalidTrackId
50+
}
51+
52+
var updatedTrack = fullTrack
53+
var extendedMetadata = updatedTrack.extendedMetadata ?? ExtendedMetadata()
54+
extendedMetadata.lyrics = lyrics
55+
updatedTrack.extendedMetadata = extendedMetadata
56+
57+
let trackToSave = updatedTrack
58+
try await dbQueue.write { db in
59+
try trackToSave.update(db)
60+
}
61+
}
4562
}

0 commit comments

Comments
 (0)