@@ -3,17 +3,35 @@ import UIKit
33
44class MusixmatchLyricsRepository : LyricsRepository {
55 private let apiUrl = " https://apic.musixmatch.com "
6-
6+
77 var selectedLanguage : String
8-
8+
99 static let shared = MusixmatchLyricsRepository (
1010 language: UserDefaults . lyricsOptions. musixmatchLanguage
1111 )
12-
12+
1313 private init ( language: String ) {
1414 selectedLanguage = language
1515 }
1616
17+ //
18+
19+ private class CachedLyrics {
20+ let dto : LyricsDto
21+
22+ init ( dto: LyricsDto ) {
23+ self . dto = dto
24+ }
25+ }
26+
27+ private let lyricsCache = NSCache < NSString , CachedLyrics > ( )
28+
29+ private func getCacheKey( for query: LyricsSearchQuery ) -> String {
30+ return " \( query. hashValue) _ \( selectedLanguage) "
31+ }
32+
33+ //
34+
1735 private func perform(
1836 _ path: String ,
1937 query: [ String : Any ] = [ : ]
@@ -26,7 +44,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
2644
2745 let queryString = finalQuery. queryString
2846 stringUrl += " ? \( queryString) "
29-
47+
3048 let request = URLRequest ( url: URL ( string: stringUrl) !)
3149
3250 let semaphore = DispatchSemaphore ( value: 0 )
@@ -48,9 +66,9 @@ class MusixmatchLyricsRepository: LyricsRepository {
4866
4967 return data!
5068 }
51-
69+
5270 //
53-
71+
5472 private func getMacroCalls( _ data: Data ) throws -> [ String : Any ] {
5573 guard
5674 let json = try ? JSONSerialization . jsonObject ( with: data, options: [ ] ) as? [ String : Any ] ,
@@ -62,41 +80,44 @@ class MusixmatchLyricsRepository: LyricsRepository {
6280 }
6381
6482 if let header = message [ " header " ] as? [ String : Any ] ,
65- header [ " status_code " ] as? Int == 401 {
83+ header [ " status_code " ] as? Int == 401
84+ {
6685 throw LyricsError . invalidMusixmatchToken
6786 }
68-
87+
6988 return macroCalls
7089 }
71-
90+
7291 private func getFirstSubtitle( _ subtitlesMessage: [ String : Any ] ) throws -> [ String : Any ] {
73- guard
92+ guard
7493 let subtitlesBody = subtitlesMessage [ " body " ] as? [ String : Any ] ,
7594 let subtitleList = subtitlesBody [ " subtitle_list " ] as? [ [ String : Any ] ] ,
7695 let firstSubtitle = subtitleList. first,
7796 let subtitle = firstSubtitle [ " subtitle " ] as? [ String : Any ]
7897 else {
7998 throw LyricsError . decodingError
8099 }
81-
100+
82101 if let restricted = subtitle [ " restricted " ] as? Bool , restricted {
83102 throw LyricsError . musixmatchRestricted
84103 }
85-
104+
86105 return subtitle
87106 }
88-
107+
89108 //
90-
91- private func getTranslations( _ spotifyTrackId: String , selectedLanguage: String ) throws -> [ String : String ] {
109+
110+ private func getTranslations( _ spotifyTrackId: String , selectedLanguage: String ) throws
111+ -> [ String : String ]
112+ {
92113 let data = try perform (
93114 " /ws/1.1/crowd.track.translations.get " ,
94115 query: [
95116 " track_spotify_id " : spotifyTrackId,
96- " selected_language " : selectedLanguage
117+ " selected_language " : selectedLanguage,
97118 ]
98119 )
99-
120+
100121 guard
101122 let json = try ? JSONSerialization . jsonObject ( with: data, options: [ ] ) as? [ String : Any ] ,
102123 let message = json [ " message " ] as? [ String : Any ] ,
@@ -109,27 +130,34 @@ class MusixmatchLyricsRepository: LyricsRepository {
109130 let translations = translationsList. compactMap {
110131 $0 [ " translation " ] as? [ String : Any ]
111132 }
112-
133+
113134 return translations. reduce ( into: [ : ] ) { dictionary, translation in
114- dictionary [ translation [ " subtitle_matched_line " ] as! String ] = translation [ " description " ] as? String
135+ dictionary [ translation [ " subtitle_matched_line " ] as! String ] =
136+ translation [ " description " ] as? String
115137 }
116138 }
117-
139+
118140 //
119-
141+
120142 func getLyrics( _ query: LyricsSearchQuery , options: LyricsOptions ) throws -> LyricsDto {
143+ let cacheKey = getCacheKey ( for: query)
144+
145+ if let cached = lyricsCache. object ( forKey: cacheKey as NSString ) {
146+ return cached. dto
147+ }
148+
121149 var musixmatchQuery = [
122150 " track_spotify_id " : query. spotifyTrackId,
123151 " subtitle_format " : " mxm " ,
124152 " q_track " : query. title,
125- " q_artist " : query. primaryArtist
153+ " q_artist " : query. primaryArtist,
126154 ]
127-
155+
128156 if !selectedLanguage. isEmpty {
129157 musixmatchQuery [ " selected_language " ] = selectedLanguage
130158 musixmatchQuery [ " part " ] = " subtitle_translated "
131159 }
132-
160+
133161 let data = try perform (
134162 " /ws/1.1/macro.subtitles.get " ,
135163 query: musixmatchQuery
@@ -139,7 +167,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
139167
140168 var romanized = false
141169 var translation : LyricsTranslationDto ? = nil
142-
170+
143171 let macroCalls = try getMacroCalls ( data)
144172
145173 if let trackSubtitlesGet = macroCalls [ " track.subtitles.get " ] as? [ String : Any ] ,
@@ -149,55 +177,55 @@ class MusixmatchLyricsRepository: LyricsRepository {
149177 let subtitleBody = subtitle [ " subtitle_body " ] as? String ,
150178 let subtitles = try ? JSONDecoder ( ) . decode (
151179 [ MusixmatchSubtitle ] . self, from: subtitleBody. data ( using: . utf8) !
152- ) {
153-
180+ )
181+ {
182+
154183 let romanizationLanguage = " r \( subtitleLanguage. prefix ( 1 ) ) "
155-
184+
156185 var lyricsLines = subtitles. dropLast ( ) . map { subtitle in
157186 LyricsLineDto (
158187 content: subtitle. text. lyricsNoteIfEmpty,
159188 offsetMs: Int ( subtitle. time. total * 1000 )
160189 )
161190 }
162-
191+
163192 lyricsLines. append (
164193 LyricsLineDto (
165194 content: " " ,
166195 offsetMs: Int ( subtitles. last!. time. total * 1000 )
167196 )
168197 )
169-
198+
170199 if selectedLanguage != subtitleLanguage,
171- let subtitleTranslated = subtitle [ " subtitle_translated " ] as? [ String : Any ] ,
172- let subtitleTranslatedBody = subtitleTranslated [ " subtitle_body " ] as? String ,
173- let subtitlesTranslated = try ? JSONDecoder ( ) . decode (
200+ let subtitleTranslated = subtitle [ " subtitle_translated " ] as? [ String : Any ] ,
201+ let subtitleTranslatedBody = subtitleTranslated [ " subtitle_body " ] as? String ,
202+ let subtitlesTranslated = try ? JSONDecoder ( ) . decode (
174203 [ MusixmatchSubtitle ] . self, from: subtitleTranslatedBody. data ( using: . utf8) !
175- )
204+ )
176205 {
177206 if selectedLanguage == romanizationLanguage {
178207 romanized = true
179-
208+
180209 for (index, subtitleTranslated) in subtitlesTranslated. enumerated ( ) {
181210 if !subtitleTranslated. text. isEmpty {
182211 lyricsLines [ index] . content = subtitleTranslated. text
183212 }
184213 }
185- }
186- else {
214+ } else {
187215 translation = LyricsTranslationDto (
188216 languageCode: selectedLanguage,
189217 lines: subtitlesTranslated. map { $0. text }
190218 )
191219 }
192220 }
193-
221+
194222 if options. romanization && selectedLanguage != romanizationLanguage {
195223 if let translations = try ? getTranslations (
196224 query. spotifyTrackId,
197225 selectedLanguage: romanizationLanguage
198226 ) {
199227 romanized = true
200-
228+
201229 for (original, translation) in translations {
202230 for i in 0 ..< lyricsLines. count {
203231 if lyricsLines [ i] . content == original {
@@ -207,50 +235,59 @@ class MusixmatchLyricsRepository: LyricsRepository {
207235 }
208236 }
209237 }
210-
238+
211239 var romanization = LyricsRomanizationStatus . original
212-
240+
213241 if romanized {
214242 romanization = . romanized
215- }
216- else if subtitleLanguage. isCanBeRomanizedLanguage {
243+ } else if subtitleLanguage. isCanBeRomanizedLanguage {
217244 romanization = . canBeRomanized
218245 }
219-
220- return LyricsDto (
246+
247+ let lyricsDto = LyricsDto (
221248 lines: lyricsLines,
222249 timeSynced: true ,
223250 romanization: romanization,
224251 translation: translation
225252 )
253+
254+ lyricsCache. setObject ( CachedLyrics ( dto: lyricsDto) , forKey: cacheKey as NSString )
255+ return lyricsDto
226256 }
227-
257+
228258 if let trackLyricsGet = macroCalls [ " track.lyrics.get " ] as? [ String : Any ] ,
229- let lyricsMessage = trackLyricsGet [ " message " ] as? [ String : Any ] ,
230- let lyricsHeader = lyricsMessage [ " header " ] as? [ String : Any ] ,
231- let lyricsStatusCode = lyricsHeader [ " status_code " ] as? Int {
232-
259+ let lyricsMessage = trackLyricsGet [ " message " ] as? [ String : Any ] ,
260+ let lyricsHeader = lyricsMessage [ " header " ] as? [ String : Any ] ,
261+ let lyricsStatusCode = lyricsHeader [ " status_code " ] as? Int
262+ {
263+
233264 if lyricsStatusCode == 404 {
234265 throw LyricsError . noSuchSong
235266 }
236-
267+
237268 if let lyricsBody = lyricsMessage [ " body " ] as? [ String : Any ] ,
238- let lyrics = lyricsBody [ " lyrics " ] as? [ String : Any ] ,
239- let lyricsLanguage = lyrics [ " lyrics_language " ] as? String ,
240- let plainLyrics = lyrics [ " lyrics_body " ] as? String {
241-
269+ let lyrics = lyricsBody [ " lyrics " ] as? [ String : Any ] ,
270+ let lyricsLanguage = lyrics [ " lyrics_language " ] as? String ,
271+ let plainLyrics = lyrics [ " lyrics_body " ] as? String
272+ {
273+
242274 if let restricted = lyrics [ " restricted " ] as? Bool , restricted {
243275 throw LyricsError . musixmatchRestricted
244276 }
245-
246- return LyricsDto (
247- lines: plainLyrics
277+
278+ let lyricsDto = LyricsDto (
279+ lines:
280+ plainLyrics
248281 . components ( separatedBy: " \n " )
249282 . dropLast ( )
250283 . map { LyricsLineDto ( content: $0. lyricsNoteIfEmpty) } ,
251284 timeSynced: false ,
252- romanization: lyricsLanguage. isCanBeRomanizedLanguage ? . canBeRomanized : . original
285+ romanization: lyricsLanguage. isCanBeRomanizedLanguage
286+ ? . canBeRomanized : . original
253287 )
288+
289+ lyricsCache. setObject ( CachedLyrics ( dto: lyricsDto) , forKey: cacheKey as NSString )
290+ return lyricsDto
254291 }
255292 }
256293
0 commit comments