mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-09 00:23:20 +01:00
162 lines
4.7 KiB
Swift
162 lines
4.7 KiB
Swift
import Foundation
|
|
|
|
class GeniusLyricsRepository: LyricsRepository {
|
|
private let jsonDecoder: JSONDecoder
|
|
private let apiUrl = "https://api.genius.com"
|
|
private let session: URLSession
|
|
|
|
init() {
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.httpAdditionalHeaders = [
|
|
"X-Genius-iOS-Version": "6.21.0",
|
|
"X-Genius-Logged-Out": "true",
|
|
"User-Agent": "Genius/1109 \(URLSessionHelper.CFNetworkVersion) \(URLSessionHelper.DarwinVersion)"
|
|
]
|
|
|
|
session = URLSession(configuration: configuration)
|
|
|
|
jsonDecoder = JSONDecoder()
|
|
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
}
|
|
|
|
private func perform(
|
|
_ path: String,
|
|
query: [String:Any] = [:]
|
|
) throws -> GeniusDataResponse? {
|
|
var stringUrl = "\(apiUrl)\(path)"
|
|
|
|
if !query.isEmpty {
|
|
let queryString = query.queryString
|
|
stringUrl += "?\(queryString)"
|
|
}
|
|
|
|
let request = URLRequest(url: URL(string: stringUrl)!)
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var data: Data?
|
|
var error: Error?
|
|
|
|
let task = session.dataTask(with: request) { response, _, err in
|
|
error = err
|
|
data = response
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
if let error = error {
|
|
throw error
|
|
}
|
|
|
|
guard let rootResponse = try? jsonDecoder.decode(GeniusRootResponse.self, from: data!) else {
|
|
throw LyricsError.decodingError
|
|
}
|
|
return rootResponse.response
|
|
}
|
|
|
|
//
|
|
|
|
private func searchSong(_ query: String) throws -> [GeniusHit] {
|
|
let data = try perform("/search/song", query: ["q": query])
|
|
|
|
guard
|
|
case .sections(let sectionsResponse) = data,
|
|
let section = sectionsResponse.sections.first
|
|
else {
|
|
throw LyricsError.decodingError
|
|
}
|
|
|
|
return section.hits
|
|
}
|
|
|
|
private func getSongInfo(_ songId: Int) throws -> GeniusSong {
|
|
let data = try perform("/songs/\(songId)", query: ["text_format": "plain"])
|
|
|
|
guard case .song(let songResponse) = data else {
|
|
throw LyricsError.decodingError
|
|
}
|
|
|
|
return songResponse.song
|
|
}
|
|
|
|
//
|
|
|
|
private func mostRelevantHitResult(
|
|
hits: [GeniusHit],
|
|
strippedTitle: String,
|
|
romanized: Bool,
|
|
hasFoundRomanizedLyrics: inout Bool
|
|
) -> GeniusHitResult {
|
|
let results = hits.map { $0.result }
|
|
|
|
let matchingByTitle = results.filter(
|
|
{ $0.title.containsInsensitive(strippedTitle) }
|
|
)
|
|
|
|
if matchingByTitle.isEmpty {
|
|
return results.first!
|
|
}
|
|
|
|
if romanized, let romanizedSong = matchingByTitle.first(
|
|
where: { $0.artistNames == "Genius Romanizations" }
|
|
) {
|
|
hasFoundRomanizedLyrics = true
|
|
return romanizedSong
|
|
}
|
|
|
|
return matchingByTitle.first!
|
|
}
|
|
|
|
private func mapLyricsLines(_ rawLines: [String]) -> [String] {
|
|
var lines = rawLines
|
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
|
|
lines.removeAll { $0 ~= "\\[.*\\]" }
|
|
|
|
lines = Array(
|
|
lines
|
|
.drop(while: { $0.isEmpty })
|
|
.dropLast(while: { $0.isEmpty })
|
|
)
|
|
|
|
return lines
|
|
}
|
|
|
|
func getLyrics(_ query: LyricsSearchQuery, options: LyricsOptions) throws -> LyricsDto {
|
|
let strippedTitle = query.title.strippedTrackTitle
|
|
let hits = try searchSong("\(strippedTitle) \(query.primaryArtist)")
|
|
|
|
guard !hits.isEmpty else {
|
|
throw LyricsError.noSuchSong
|
|
}
|
|
|
|
var hasFoundRomanizedLyrics = false
|
|
|
|
let song = mostRelevantHitResult(
|
|
hits: hits,
|
|
strippedTitle: strippedTitle,
|
|
romanized: options.romanization,
|
|
hasFoundRomanizedLyrics: &hasFoundRomanizedLyrics
|
|
)
|
|
|
|
let songInfo = try getSongInfo(song.id)
|
|
let plainLines = songInfo.lyrics.plain.components(separatedBy: "\n")
|
|
|
|
var romanization = LyricsRomanizationStatus.original
|
|
|
|
if hasFoundRomanizedLyrics {
|
|
romanization = .romanized
|
|
}
|
|
else if songInfo.language.isCanBeRomanizedLanguage {
|
|
romanization = .canBeRomanized
|
|
}
|
|
|
|
return LyricsDto(
|
|
lines: mapLyricsLines(plainLines).map { line in LyricsLineDto(content: line) },
|
|
timeSynced: false,
|
|
romanization: romanization
|
|
)
|
|
}
|
|
}
|