diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift index 20a3e688..89fa1686 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift @@ -26,7 +26,7 @@ class LyricsFullscreenViewControllerHook: ClassHook { if UserDefaults.lyricsSource == .musixmatch && lastLyricsError == nil - && lastLyricsLanguageLabel == nil { + && !lastLyricsWasRomanized { return } @@ -40,7 +40,7 @@ class LyricsFullscreenViewControllerHook: ClassHook { // -private var lastLyricsLanguageLabel: String? = nil +private var lastLyricsWasRomanized = false private var lastLyricsError: LyricsError? = nil private var hasShownRestrictedPopUp = false @@ -107,11 +107,11 @@ class LyricsOnlyViewControllerHook: ClassHook { ) } - if let languageLabel = lastLyricsLanguageLabel { + if lastLyricsWasRomanized { text.append( Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self) .initWithString( - "\n\(languageLabel)", + "\nRomanized", typeStyle: typeStyle, attributes: attributes ) @@ -206,9 +206,7 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data { lyricsDto = try repository.getLyrics(searchQuery, options: options) } - lastLyricsLanguageLabel = lyricsDto.romanized - ? "Romanized" - : Locale.current.localizedString(forLanguageCode: lyricsDto.translatedTo ?? "") + lastLyricsWasRomanized = lyricsDto.romanized let lyrics = Lyrics.with { $0.colors = getLyricsColors() diff --git a/Sources/EeveeSpotify/Lyrics/Models/Lyrics.pb.swift b/Sources/EeveeSpotify/Lyrics/Models/Lyrics.pb.swift index 6dcc1dfc..5ac572f5 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Lyrics.pb.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Lyrics.pb.swift @@ -98,6 +98,20 @@ struct LyricsColors { init() {} } +struct LyricsTranslation { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var languageCode: String = String() + + var lines: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + struct LyricsData { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -111,9 +125,20 @@ struct LyricsData { var restriction: LyricsRestriction = .unrestricted + var translation: LyricsTranslation { + get {return _translation ?? LyricsTranslation()} + set {_translation = newValue} + } + /// Returns true if `translation` has been explicitly set. + var hasTranslation: Bool {return self._translation != nil} + /// Clears the value of `translation`. Subsequent reads from it will return its default value. + mutating func clearTranslation() {self._translation = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} + + fileprivate var _translation: LyricsTranslation? = nil } struct Lyrics { @@ -151,6 +176,7 @@ struct Lyrics { extension LyricsRestriction: @unchecked Sendable {} extension LyricsLine: @unchecked Sendable {} extension LyricsColors: @unchecked Sendable {} +extension LyricsTranslation: @unchecked Sendable {} extension LyricsData: @unchecked Sendable {} extension Lyrics: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) @@ -248,6 +274,44 @@ extension LyricsColors: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } } +extension LyricsTranslation: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "LyricsTranslation" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "languageCode"), + 2: .same(proto: "lines"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.languageCode) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.lines) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.languageCode.isEmpty { + try visitor.visitSingularStringField(value: self.languageCode, fieldNumber: 1) + } + if !self.lines.isEmpty { + try visitor.visitRepeatedStringField(value: self.lines, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: LyricsTranslation, rhs: LyricsTranslation) -> Bool { + if lhs.languageCode != rhs.languageCode {return false} + if lhs.lines != rhs.lines {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "LyricsData" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -255,6 +319,7 @@ extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 2: .same(proto: "lines"), 5: .same(proto: "providedBy"), 14: .same(proto: "restriction"), + 9: .same(proto: "translation"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -266,6 +331,7 @@ extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 1: try { try decoder.decodeSingularBoolField(value: &self.timeSynchronized) }() case 2: try { try decoder.decodeRepeatedMessageField(value: &self.lines) }() case 5: try { try decoder.decodeSingularStringField(value: &self.providedBy) }() + case 9: try { try decoder.decodeSingularMessageField(value: &self._translation) }() case 14: try { try decoder.decodeSingularEnumField(value: &self.restriction) }() default: break } @@ -273,6 +339,10 @@ extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if self.timeSynchronized != false { try visitor.visitSingularBoolField(value: self.timeSynchronized, fieldNumber: 1) } @@ -282,6 +352,9 @@ extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if !self.providedBy.isEmpty { try visitor.visitSingularStringField(value: self.providedBy, fieldNumber: 5) } + try { if let v = self._translation { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() if self.restriction != .unrestricted { try visitor.visitSingularEnumField(value: self.restriction, fieldNumber: 14) } @@ -293,6 +366,7 @@ extension LyricsData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.lines != rhs.lines {return false} if lhs.providedBy != rhs.providedBy {return false} if lhs.restriction != rhs.restriction {return false} + if lhs._translation != rhs._translation {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/EeveeSpotify/Lyrics/Models/LyricsDto.swift b/Sources/EeveeSpotify/Lyrics/Models/LyricsDto.swift index 733e25a9..88e8cacb 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/LyricsDto.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/LyricsDto.swift @@ -4,10 +4,10 @@ struct LyricsDto { var lines: [LyricsLineDto] var timeSynced: Bool var romanized: Bool = false - var translatedTo: String? = nil + var translation: LyricsTranslationDto? func toLyricsData(source: String) -> LyricsData { - return LyricsData.with { + var lyricsData = LyricsData.with { $0.timeSynchronized = timeSynced $0.restriction = .unrestricted $0.providedBy = "\(source) (EeveeSpotify)" @@ -18,5 +18,14 @@ struct LyricsDto { } } } + + if let translation = translation { + lyricsData.translation = LyricsTranslation.with { + $0.languageCode = translation.languageCode + $0.lines = translation.lines + } + } + + return lyricsData } } diff --git a/Sources/EeveeSpotify/Lyrics/Models/LyricsTranslationDto.swift b/Sources/EeveeSpotify/Lyrics/Models/LyricsTranslationDto.swift new file mode 100644 index 00000000..cf1bd77b --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/LyricsTranslationDto.swift @@ -0,0 +1,6 @@ +import Foundation + +struct LyricsTranslationDto { + var languageCode: String + var lines: [String] +} diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift index f51f4153..97968b64 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift @@ -153,7 +153,7 @@ class MusixmatchLyricsRepository: LyricsRepository { // 😭😭😭 var romanized = false - var translatedTo: String? = nil + var translation: LyricsTranslationDto? = nil let macroCalls = try getMacroCalls(data) @@ -164,40 +164,43 @@ class MusixmatchLyricsRepository: LyricsRepository { let subtitleBody = subtitle["subtitle_body"] as? String, let subtitles = try? JSONDecoder().decode( [MusixmatchSubtitle].self, from: subtitleBody.data(using: .utf8)! - ).dropLast() { + ) { - var lyricsLines: [LyricsLineDto] 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 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)! - ).dropLast() { - - lyricsLines = subtitlesTranslated.enumerated().map { (index, subtitleTranslated) in - let content = subtitleTranslated.text.isEmpty - ? subtitles[index].text - : subtitleTranslated.text - - return LyricsLineDto( - content: content.lyricsNoteIfEmpty, - offsetMs: Int(subtitleTranslated.time.total * 1000) - ) - } + ) { if selectedLanguage == romanizationLanguage { romanized = true + + for (index, subtitleTranslated) in subtitlesTranslated.enumerated() { + if !subtitleTranslated.text.isEmpty { + lyricsLines[index].content = subtitleTranslated.text + } + } } else { - translatedTo = selectedLanguage - } - } - else { - lyricsLines = subtitles.map { subtitle in - LyricsLineDto( - content: subtitle.text.lyricsNoteIfEmpty, - offsetMs: Int(subtitle.time.total * 1000) + translation = LyricsTranslationDto( + languageCode: selectedLanguage, + lines: subtitlesTranslated.map { $0.text } ) } } @@ -212,24 +215,23 @@ class MusixmatchLyricsRepository: LyricsRepository { query.spotifyTrackId, selectedLanguage: romanizationLanguage ) { + romanized = true + for (original, translation) in translations { - for i in 0..