From 85a002b9b332845bcb25bbc82e659b09d31e732e Mon Sep 17 00:00:00 2001 From: Mapleshade <62641761+Mapleshade20@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:37:24 +0800 Subject: [PATCH] fix: improve lyrics fetching and query encoding for special characters (#594) * chore: gitignore .vscode * fix(lrclib): use 'get' API, return empty lines when instrumental * build: add workflow_dispatch trigger * fix: encode query strings with stricter character set --- .github/workflows/build-swift.yml | 1 + .gitignore | 1 + .../Lyrics/Models/Lrclib/LrclibSong.swift | 1 + .../Repositories/GeniusLyricsRepository.swift | 4 +- .../Repositories/LrclibLyricsRepository.swift | 49 +++++++++++-------- .../Extensions/CharacterSet+Extension.swift | 9 ++++ .../Extensions/Dictionary+Extension.swift | 10 ++-- 7 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 Sources/EeveeSpotify/Models/Extensions/CharacterSet+Extension.swift diff --git a/.github/workflows/build-swift.yml b/.github/workflows/build-swift.yml index 2f27402..8ef25da 100644 --- a/.github/workflows/build-swift.yml +++ b/.github/workflows/build-swift.yml @@ -5,6 +5,7 @@ on: branches: [ "swift" ] pull_request: branches: [ "swift" ] + workflow_dispatch: jobs: build: diff --git a/.gitignore b/.gitignore index a09d227..a9feab9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ xcuserdata/ .theos/ packages/ .DS_Store +.vscode/ diff --git a/Sources/EeveeSpotify/Lyrics/Models/Lrclib/LrclibSong.swift b/Sources/EeveeSpotify/Lyrics/Models/Lrclib/LrclibSong.swift index 662c04d..30d95ef 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Lrclib/LrclibSong.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Lrclib/LrclibSong.swift @@ -4,4 +4,5 @@ struct LrclibSong: Decodable { var name: String var plainLyrics: String? var syncedLyrics: String? + var instrumental: Bool } \ No newline at end of file diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift index 85f83a5..deb66b9 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift @@ -26,9 +26,7 @@ struct GeniusLyricsRepository: LyricsRepository { var stringUrl = "\(apiUrl)\(path)" if !query.isEmpty { - let queryString = query.queryString.addingPercentEncoding( - withAllowedCharacters: .urlHostAllowed - )! + let queryString = query.queryString stringUrl += "?\(queryString)" } diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift index 5563b6b..c7736cf 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift @@ -20,9 +20,7 @@ struct LrcLibLyricsRepository: LyricsRepository { var stringUrl = "\(apiUrl)\(path)" if !query.isEmpty { - let queryString = query.queryString.addingPercentEncoding( - withAllowedCharacters: .urlHostAllowed - )! + let queryString = query.queryString stringUrl += "?\(queryString)" } @@ -49,23 +47,18 @@ struct LrcLibLyricsRepository: LyricsRepository { return data! } - private func searchSong(_ query: String) throws -> [LrclibSong] { - let data = try perform("/search", query: ["q": query]) - return try JSONDecoder().decode([LrclibSong].self, from: data) - } - - // - - private func mostRelevantSong(songs: [LrclibSong], strippedTitle: String) -> LrclibSong? { - return songs.first( - where: { $0.name.containsInsensitive(strippedTitle) } - ) ?? songs.first + private func getSong(trackName: String, artistName: String) throws -> LrclibSong { + let data: Data = try perform("/get", query: [ + "track_name": trackName, + "artist_name": artistName + ]) + return try JSONDecoder().decode(LrclibSong.self, from: data) } private func mapSyncedLyricsLines(_ lines: [String]) -> [LyricsLineDto] { return lines.compactMap { line in guard let match = line.firstMatch( - "\\[(?\\d*):(?\\d*\\.?\\d*)\\] ?(?.*)" + "\\[(?\\d*):(?\\d+\\.\\d+|\\d+)\\] ?(?.*)" ) else { return nil } @@ -93,13 +86,27 @@ struct LrcLibLyricsRepository: LyricsRepository { } func getLyrics(_ query: LyricsSearchQuery, options: LyricsOptions) throws -> LyricsDto { - let strippedTitle = query.title.strippedTrackTitle - let songs = try searchSong("\(strippedTitle) \(query.primaryArtist)") - - guard let song = mostRelevantSong(songs: songs, strippedTitle: strippedTitle) else { - throw LyricsError.noSuchSong + let song: LrclibSong + + do { + song = try getSong(trackName: query.title, artistName: query.primaryArtist) + } catch { + let strippedTitle = query.title.strippedTrackTitle + do { + song = try getSong(trackName: strippedTitle, artistName: query.primaryArtist) + } catch { + throw LyricsError.noSuchSong + } } - + + if song.instrumental { + return LyricsDto( + lines: [], + timeSynced: false, + romanization: .original + ) + } + if let syncedLyrics = song.syncedLyrics { let lines = Array(syncedLyrics.components(separatedBy: "\n").dropLast()) return LyricsDto( diff --git a/Sources/EeveeSpotify/Models/Extensions/CharacterSet+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/CharacterSet+Extension.swift new file mode 100644 index 0000000..ed1f78d --- /dev/null +++ b/Sources/EeveeSpotify/Models/Extensions/CharacterSet+Extension.swift @@ -0,0 +1,9 @@ +import Foundation + +extension CharacterSet { + static var urlQueryAllowedStrict: CharacterSet { + var allowed = CharacterSet.urlQueryAllowed + allowed.remove(charactersIn: "/?@&=+$") + return allowed + } +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift index b866c2f..a604f99 100644 --- a/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift @@ -3,9 +3,13 @@ import Foundation extension Dictionary { var queryString: String { return self - .compactMap({ (key, value) -> String in - return "\(key)=\(value)" + .compactMap({ (key, value) -> String? in + guard let keyString = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedStrict), + let valueString = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedStrict) else { + return nil + } + return "\(keyString)=\(valueString)" }) - .joined(separator: "&") + .joined(separator: "&") } } \ No newline at end of file