diff --git a/Sources/EeveeSpotify/Lyrics/Models/LyricsSearchQuery.swift b/Sources/EeveeSpotify/Lyrics/Models/LyricsSearchQuery.swift index 78569eb..eb71a52 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/LyricsSearchQuery.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/LyricsSearchQuery.swift @@ -1,6 +1,6 @@ import Foundation -struct LyricsSearchQuery { +struct LyricsSearchQuery: Hashable { var title: String var primaryArtist: String var spotifyTrackId: String diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift index aa0e66e..93c5586 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift @@ -3,17 +3,35 @@ import UIKit class MusixmatchLyricsRepository: LyricsRepository { private let apiUrl = "https://apic.musixmatch.com" - + var selectedLanguage: String - + static let shared = MusixmatchLyricsRepository( language: UserDefaults.lyricsOptions.musixmatchLanguage ) - + private init(language: String) { selectedLanguage = language } + // + + private class CachedLyrics { + let dto: LyricsDto + + init(dto: LyricsDto) { + self.dto = dto + } + } + + private let lyricsCache = NSCache() + + private func getCacheKey(for query: LyricsSearchQuery) -> String { + return "\(query.hashValue)_\(selectedLanguage)" + } + + // + private func perform( _ path: String, query: [String: Any] = [:] @@ -26,7 +44,7 @@ class MusixmatchLyricsRepository: LyricsRepository { let queryString = finalQuery.queryString stringUrl += "?\(queryString)" - + let request = URLRequest(url: URL(string: stringUrl)!) let semaphore = DispatchSemaphore(value: 0) @@ -48,9 +66,9 @@ class MusixmatchLyricsRepository: LyricsRepository { return data! } - + // - + private func getMacroCalls(_ data: Data) throws -> [String: Any] { guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], @@ -62,15 +80,16 @@ class MusixmatchLyricsRepository: LyricsRepository { } if let header = message["header"] as? [String: Any], - header["status_code"] as? Int == 401 { + header["status_code"] as? Int == 401 + { throw LyricsError.invalidMusixmatchToken } - + return macroCalls } - + private func getFirstSubtitle(_ subtitlesMessage: [String: Any]) throws -> [String: Any] { - guard + guard let subtitlesBody = subtitlesMessage["body"] as? [String: Any], let subtitleList = subtitlesBody["subtitle_list"] as? [[String: Any]], let firstSubtitle = subtitleList.first, @@ -78,25 +97,27 @@ class MusixmatchLyricsRepository: LyricsRepository { else { throw LyricsError.decodingError } - + if let restricted = subtitle["restricted"] as? Bool, restricted { throw LyricsError.musixmatchRestricted } - + return subtitle } - + // - - private func getTranslations(_ spotifyTrackId: String, selectedLanguage: String) throws -> [String: String] { + + private func getTranslations(_ spotifyTrackId: String, selectedLanguage: String) throws + -> [String: String] + { let data = try perform( "/ws/1.1/crowd.track.translations.get", query: [ "track_spotify_id": spotifyTrackId, - "selected_language": selectedLanguage + "selected_language": selectedLanguage, ] ) - + guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let message = json["message"] as? [String: Any], @@ -109,27 +130,34 @@ class MusixmatchLyricsRepository: LyricsRepository { let translations = translationsList.compactMap { $0["translation"] as? [String: Any] } - + return translations.reduce(into: [:]) { dictionary, translation in - dictionary[translation["subtitle_matched_line"] as! String] = translation["description"] as? String + dictionary[translation["subtitle_matched_line"] as! String] = + translation["description"] as? String } } - + // - + func getLyrics(_ query: LyricsSearchQuery, options: LyricsOptions) throws -> LyricsDto { + let cacheKey = getCacheKey(for: query) + + if let cached = lyricsCache.object(forKey: cacheKey as NSString) { + return cached.dto + } + var musixmatchQuery = [ "track_spotify_id": query.spotifyTrackId, "subtitle_format": "mxm", "q_track": query.title, - "q_artist": query.primaryArtist + "q_artist": query.primaryArtist, ] - + if !selectedLanguage.isEmpty { musixmatchQuery["selected_language"] = selectedLanguage musixmatchQuery["part"] = "subtitle_translated" } - + let data = try perform( "/ws/1.1/macro.subtitles.get", query: musixmatchQuery @@ -139,7 +167,7 @@ class MusixmatchLyricsRepository: LyricsRepository { var romanized = false var translation: LyricsTranslationDto? = nil - + let macroCalls = try getMacroCalls(data) if let trackSubtitlesGet = macroCalls["track.subtitles.get"] as? [String: Any], @@ -149,55 +177,55 @@ class MusixmatchLyricsRepository: LyricsRepository { let subtitleBody = subtitle["subtitle_body"] as? String, let subtitles = try? JSONDecoder().decode( [MusixmatchSubtitle].self, from: subtitleBody.data(using: .utf8)! - ) { - + ) + { + let romanizationLanguage = "r\(subtitleLanguage.prefix(1))" - + var lyricsLines = subtitles.dropLast().map { subtitle in LyricsLineDto( content: subtitle.text.lyricsNoteIfEmpty, offsetMs: Int(subtitle.time.total * 1000) ) } - + lyricsLines.append( LyricsLineDto( content: "", offsetMs: Int(subtitles.last!.time.total * 1000) ) ) - + if selectedLanguage != subtitleLanguage, - let subtitleTranslated = subtitle["subtitle_translated"] as? [String: Any], - let subtitleTranslatedBody = subtitleTranslated["subtitle_body"] as? String, - let subtitlesTranslated = try? JSONDecoder().decode( + let subtitleTranslated = subtitle["subtitle_translated"] as? [String: Any], + let subtitleTranslatedBody = subtitleTranslated["subtitle_body"] as? String, + let subtitlesTranslated = try? JSONDecoder().decode( [MusixmatchSubtitle].self, from: subtitleTranslatedBody.data(using: .utf8)! - ) + ) { if selectedLanguage == romanizationLanguage { romanized = true - + for (index, subtitleTranslated) in subtitlesTranslated.enumerated() { if !subtitleTranslated.text.isEmpty { lyricsLines[index].content = subtitleTranslated.text } } - } - else { + } else { translation = LyricsTranslationDto( languageCode: selectedLanguage, lines: subtitlesTranslated.map { $0.text } ) } } - + if options.romanization && selectedLanguage != romanizationLanguage { if let translations = try? getTranslations( query.spotifyTrackId, selectedLanguage: romanizationLanguage ) { romanized = true - + for (original, translation) in translations { for i in 0..