Skip to content

Commit

Permalink
native lyrics translation
Browse files Browse the repository at this point in the history
  • Loading branch information
whoeevee committed Jul 13, 2024
1 parent 2c556da commit 98f6f9e
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 37 deletions.
12 changes: 5 additions & 7 deletions Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {

if UserDefaults.lyricsSource == .musixmatch
&& lastLyricsError == nil
&& lastLyricsLanguageLabel == nil {
&& !lastLyricsWasRomanized {
return
}

Expand All @@ -40,7 +40,7 @@ class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {

//

private var lastLyricsLanguageLabel: String? = nil
private var lastLyricsWasRomanized = false
private var lastLyricsError: LyricsError? = nil

private var hasShownRestrictedPopUp = false
Expand Down Expand Up @@ -107,11 +107,11 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
)
}

if let languageLabel = lastLyricsLanguageLabel {
if lastLyricsWasRomanized {
text.append(
Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self)
.initWithString(
"\n\(languageLabel)",
"\nRomanized",
typeStyle: typeStyle,
attributes: attributes
)
Expand Down Expand Up @@ -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()
Expand Down
74 changes: 74 additions & 0 deletions Sources/EeveeSpotify/Lyrics/Models/Lyrics.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -248,13 +274,52 @@ 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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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 = [
1: .same(proto: "timeSynchronized"),
2: .same(proto: "lines"),
5: .same(proto: "providedBy"),
14: .same(proto: "restriction"),
9: .same(proto: "translation"),
]

mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All @@ -266,13 +331,18 @@ 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
}
}
}

func traverse<V: SwiftProtobuf.Visitor>(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)
}
Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down
13 changes: 11 additions & 2 deletions Sources/EeveeSpotify/Lyrics/Models/LyricsDto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -18,5 +18,14 @@ struct LyricsDto {
}
}
}

if let translation = translation {
lyricsData.translation = LyricsTranslation.with {
$0.languageCode = translation.languageCode
$0.lines = translation.lines
}
}

return lyricsData
}
}
6 changes: 6 additions & 0 deletions Sources/EeveeSpotify/Lyrics/Models/LyricsTranslationDto.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

struct LyricsTranslationDto {
var languageCode: String
var lines: [String]
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
// 😭😭😭

var romanized = false
var translatedTo: String? = nil
var translation: LyricsTranslationDto? = nil

let macroCalls = try getMacroCalls(data)

Expand All @@ -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 }
)
}
}
Expand All @@ -212,24 +215,23 @@ class MusixmatchLyricsRepository: LyricsRepository {
query.spotifyTrackId,
selectedLanguage: romanizationLanguage
) {
romanized = true

for (original, translation) in translations {

for i in 0..<lyricsLines.count {
if lyricsLines[i].content == original {
lyricsLines[i].content = translation
}
}
}

romanized = true
}
}

return LyricsDto(
lines: lyricsLines,
timeSynced: true,
romanized: romanized,
translatedTo: translatedTo
translation: translation
)
}

Expand Down

0 comments on commit 98f6f9e

Please sign in to comment.