diff --git a/Sources/EeveeSpotify/GeniusApi.swift b/Sources/EeveeSpotify/GeniusApi.swift new file mode 100644 index 0000000..08a04a8 --- /dev/null +++ b/Sources/EeveeSpotify/GeniusApi.swift @@ -0,0 +1,63 @@ +import Foundation + +struct GeniusApi { + + private static let apiUrl = "https://api.genius.com" + + private static func perform( + _ path: String, + query: [String:Any] = [:] + ) throws -> GeniusDataResponse? { + + var stringUrl = "\(apiUrl)\(path)" + + if !query.isEmpty { + + let queryString = query.queryString.addingPercentEncoding( + withAllowedCharacters: .urlHostAllowed + )! + + stringUrl += "?\(queryString)" + } + + var request = URLRequest(url: URL(string: stringUrl)!) + request.addValue("6.19.1", forHTTPHeaderField: "X-Genius-iOS-Version") + + let semaphore = DispatchSemaphore(value: 0) + var data: Data? + + let task = URLSession.shared.dataTask(with: request) { response, _, _ in + data = response + semaphore.signal() + } + + task.resume() + semaphore.wait() + + let rootResponse = try JSONDecoder().decode(GeniusRootResponse.self, from: data!) + return rootResponse.response + } + + static func search(_ query: String) throws -> [GeniusHit] { + + let data = try perform("/search", query: ["q": query]) + + guard case .hits(let hitsResponse) = data else { + throw GeniusLyricsError.DecodingError + } + + return hitsResponse.hits + } + + static func getSongInfo(_ songId: Int) throws -> GeniusSong { + + let data = try perform("/songs/\(songId)", query: ["text_format": "plain"]) + + guard case .song(let songResponse) = data else { + throw GeniusLyricsError.DecodingError + } + + return songResponse.song + } + +} diff --git a/Sources/EeveeSpotify/GeniusLyrics.x.swift b/Sources/EeveeSpotify/GeniusLyrics.x.swift new file mode 100644 index 0000000..5ce7370 --- /dev/null +++ b/Sources/EeveeSpotify/GeniusLyrics.x.swift @@ -0,0 +1,162 @@ +import Orion +import SwiftUI + +class SPTPlayerTrackHook: ClassHook { + + static let targetName = "SPTPlayerTrack" + + func setMetadata(_ metadata: [String:String]) { + var meta = metadata + + meta["has_lyrics"] = "true" + orig.setMetadata(meta) + } +} + +class EncoreButtonHook: ClassHook { + + static let targetName = "_TtC12EncoreMobileP33_6EF3A3C098E69FB1E331877B69ACBF8512EncoreButton" + + func intrinsicContentSize() -> CGSize { + + if target.accessibilityIdentifier == "Components.UI.LyricsHeader.ReportButton" { + target.isEnabled = false + } + + return orig.intrinsicContentSize() + } +} + +func getCurrentTrackLyricsData() throws -> Data { + + let track = HookedInstances.currentTrack! + + let title = track.trackTitle() + .removeMatches("\\(.*\\)") + .prefix(30) + + let artist = track.artistTitle() + + let geniusHits = try GeniusApi.search("\(title) \(artist)") + + guard let geniusSong = ( + geniusHits.first( + where: { $0.result.title.containsInsensitive(title) } + ) ?? geniusHits.first + )?.result else { + throw GeniusLyricsError.NoSuchSong + } + + let geniusSongInfo = try GeniusApi.getSongInfo(geniusSong.id) + + var geniusLyrics = geniusSongInfo.lyrics.plain + .components(separatedBy: "\n") + .map { $0.trimmingCharacters(in: .whitespaces) } + + geniusLyrics.removeAll { $0.matches("\\[.*\\]") } + + geniusLyrics = Array( + geniusLyrics + .drop(while: { $0.isEmpty }) + .dropLast(while: { $0.isEmpty }) + ) + + let lyrics = Lyrics.with { + $0.colors = LyricsColors.with { + $0.backgroundColor = Color(hex: track.extractedColorHex()).uInt32 + $0.lineColor = Color.black.uInt32 + $0.activeLineColor = Color.white.uInt32 + } + $0.data = LyricsData.with { + $0.timeSynchronized = false + $0.restriction = .unrestricted + $0.providedBy = "Genius (EeveeSpotify)" + $0.lines = geniusLyrics.map { line in + LyricsLine.with { $0.content = line } + } + } + } + + return try lyrics.serializedData() +} + +class SPTDataLoaderServiceHook: ClassHook { + + static let targetName = "SPTDataLoaderService" + + func URLSession( + _ session: URLSession, + dataTask task: URLSessionDataTask, + didReceiveResponse response: HTTPURLResponse, + completionHandler handler: Any + ) { + let url = response.url! + + if url.isLyrics, response.statusCode != 200 { + + let okResponse = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "2.0", + headerFields: [:] + )! + + do { + + let lyricsData = try getCurrentTrackLyricsData() + + orig.URLSession( + session, + dataTask: task, + didReceiveResponse: okResponse, + completionHandler: handler + ) + + orig.URLSession( + session, + dataTask: task, + didReceiveData: lyricsData + ) + + return + } + catch { + NSLog("[EeveeSpotify] Unable to load lyrics: \(error)") + } + } + + orig.URLSession( + session, + dataTask: task, + didReceiveResponse: response, + completionHandler: handler + ) + } + + func URLSession( + _ session: URLSession, + dataTask task: URLSessionDataTask, + didReceiveData data: Data + ) { + let request = task.currentRequest! + let url = request.url! + + if url.isLyrics { + + do { + orig.URLSession( + session, + dataTask: task, + didReceiveData: try getCurrentTrackLyricsData() + ) + + return + } + catch { + NSLog("[EeveeSpotify] Unable to load lyrics: \(error)") + } + } + + orig.URLSession(session, dataTask: task, didReceiveData: data) + } +} diff --git a/Sources/EeveeSpotify/HookedInstances.x.swift b/Sources/EeveeSpotify/HookedInstances.x.swift index cc7d65b..070a245 100644 --- a/Sources/EeveeSpotify/HookedInstances.x.swift +++ b/Sources/EeveeSpotify/HookedInstances.x.swift @@ -2,6 +2,22 @@ import Orion class HookedInstances { static var productState: SPTCoreProductState? + static var currentTrack: SPTPlayerTrack? +} + +class SPTNowPlayingContentLayerViewModelHook: ClassHook { + + static let targetName = "SPTNowPlayingContentLayerViewModel" + + func currentTrack() -> SPTPlayerTrack? { + + if let track = orig.currentTrack() { + HookedInstances.currentTrack = track + return track + } + + return nil + } } class SPTCoreProductStateInstanceHook: ClassHook { diff --git a/Sources/EeveeSpotify/Models/Extensions/Collection+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/Collection+Extension.swift new file mode 100644 index 0000000..e73e7db --- /dev/null +++ b/Sources/EeveeSpotify/Models/Extensions/Collection+Extension.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Collection { + func dropLast(while predicate: (Element) throws -> Bool) rethrows -> SubSequence { + guard let index = try indices.reversed().first(where: { try !predicate(self[$0]) }) else { + return self[startIndex..> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } + + var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { + + typealias NativeColor = UIColor + + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + guard NativeColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) else { + return (0, 0, 0, 0) + } + + return (r, g, b, a) + } + + var uInt32: UInt32 { + UInt32(components.alpha * 255) << 24 + | UInt32(components.red * 255) << 16 + | UInt32(components.green * 255) << 8 + | UInt32(components.blue * 255) + } +} + diff --git a/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift new file mode 100644 index 0000000..b866c2f --- /dev/null +++ b/Sources/EeveeSpotify/Models/Extensions/Dictionary+Extension.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Dictionary { + var queryString: String { + return self + .compactMap({ (key, value) -> String in + return "\(key)=\(value)" + }) + .joined(separator: "&") + } +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift index b900927..535a109 100644 --- a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift @@ -1,10 +1,32 @@ import Foundation extension String { - + static func ~= (lhs: String, rhs: String) -> Bool { guard let regex = try? NSRegularExpression(pattern: rhs) else { return false } let range = NSRange(location: 0, length: lhs.utf16.count) return regex.firstMatch(in: lhs, options: [], range: range) != nil } + + var range: NSRange { + NSRange(self.startIndex..., in: self) + } + + func containsInsensitive(_ s: S) -> Bool { + self.range(of: s, options: .caseInsensitive) != nil + } + + func matches(_ pattern: String) -> Bool { + try! NSRegularExpression(pattern: pattern) + .firstMatch(in: self, range: self.range) != nil + } + + func removeMatches(_ pattern: String) -> String { + try! NSRegularExpression(pattern: pattern) + .stringByReplacingMatches( + in: self, + range: self.range, + withTemplate: "" + ) + } } \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift new file mode 100644 index 0000000..44e4276 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift @@ -0,0 +1,7 @@ +import Foundation + +extension URL { + var isLyrics: Bool { + self.path.contains("color-lyrics/v2") + } +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusDataResponse.swift b/Sources/EeveeSpotify/Models/Genius/GeniusDataResponse.swift new file mode 100644 index 0000000..6dbea64 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusDataResponse.swift @@ -0,0 +1,26 @@ +import Foundation + +enum GeniusDataResponse: Decodable { + case hits(GeniusHitsResponse) + case song(GeniusSongResponse) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let hits = try? container.decode(GeniusHitsResponse.self) { + self = .hits(hits) + } + else if let song = try? container.decode(GeniusSongResponse.self) { + self = .song(song) + } + else { + throw DecodingError.typeMismatch( + GeniusDataResponse.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Invalid data format" + ) + ) + } + } +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusHit.swift b/Sources/EeveeSpotify/Models/Genius/GeniusHit.swift new file mode 100644 index 0000000..97c58b3 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusHit.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusHit: Decodable { + var result: GeniusHitResult +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusHitResult.swift b/Sources/EeveeSpotify/Models/Genius/GeniusHitResult.swift new file mode 100644 index 0000000..bcf832a --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusHitResult.swift @@ -0,0 +1,6 @@ +import Foundation + +struct GeniusHitResult: Decodable { + var id: Int + var title: String +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusHitsResponse.swift b/Sources/EeveeSpotify/Models/Genius/GeniusHitsResponse.swift new file mode 100644 index 0000000..bc17aad --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusHitsResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusHitsResponse: Decodable { + var hits: [GeniusHit] +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusLyrics.swift b/Sources/EeveeSpotify/Models/Genius/GeniusLyrics.swift new file mode 100644 index 0000000..94b0db2 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusLyrics.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusLyrics: Decodable { + var plain: String +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusRootResponse.swift b/Sources/EeveeSpotify/Models/Genius/GeniusRootResponse.swift new file mode 100644 index 0000000..41ac505 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusRootResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusRootResponse : Decodable { + var response: GeniusDataResponse? +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusSong.swift b/Sources/EeveeSpotify/Models/Genius/GeniusSong.swift new file mode 100644 index 0000000..98bb348 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusSong.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusSong: Decodable { + var lyrics: GeniusLyrics +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/Genius/GeniusSongResponse.swift b/Sources/EeveeSpotify/Models/Genius/GeniusSongResponse.swift new file mode 100644 index 0000000..d0b5d9a --- /dev/null +++ b/Sources/EeveeSpotify/Models/Genius/GeniusSongResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +struct GeniusSongResponse: Decodable { + var song: GeniusSong +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/GeniusLyricsError.swift b/Sources/EeveeSpotify/Models/GeniusLyricsError.swift new file mode 100644 index 0000000..f043c38 --- /dev/null +++ b/Sources/EeveeSpotify/Models/GeniusLyricsError.swift @@ -0,0 +1,6 @@ +import Foundation + +enum GeniusLyricsError: Swift.Error { + case DecodingError + case NoSuchSong +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/SPTCoreProductState.swift b/Sources/EeveeSpotify/Models/Headers/SPTCoreProductState.swift similarity index 100% rename from Sources/EeveeSpotify/Models/SPTCoreProductState.swift rename to Sources/EeveeSpotify/Models/Headers/SPTCoreProductState.swift diff --git a/Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift b/Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpDialog.swift similarity index 100% rename from Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift rename to Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpDialog.swift diff --git a/Sources/EeveeSpotify/Models/SPTEncorePopUpDialogModel.swift b/Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpDialogModel.swift similarity index 100% rename from Sources/EeveeSpotify/Models/SPTEncorePopUpDialogModel.swift rename to Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpDialogModel.swift diff --git a/Sources/EeveeSpotify/Models/SPTEncorePopUpPresenter.swift b/Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpPresenter.swift similarity index 100% rename from Sources/EeveeSpotify/Models/SPTEncorePopUpPresenter.swift rename to Sources/EeveeSpotify/Models/Headers/SPTEncorePopUpPresenter.swift diff --git a/Sources/EeveeSpotify/Models/Headers/SPTPlayerTrack.swift b/Sources/EeveeSpotify/Models/Headers/SPTPlayerTrack.swift new file mode 100644 index 0000000..10d9422 --- /dev/null +++ b/Sources/EeveeSpotify/Models/Headers/SPTPlayerTrack.swift @@ -0,0 +1,8 @@ +import Foundation + +@objc protocol SPTPlayerTrack { + func setMetadata(_ metadata: [String:String]) + func extractedColorHex() -> String + func trackTitle() -> String + func artistTitle() -> String +} diff --git a/Sources/EeveeSpotify/Models/Lyrics.pb.swift b/Sources/EeveeSpotify/Models/Lyrics.pb.swift new file mode 100644 index 0000000..6dcc1df --- /dev/null +++ b/Sources/EeveeSpotify/Models/Lyrics.pb.swift @@ -0,0 +1,341 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: lyrics.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum LyricsRestriction: SwiftProtobuf.Enum { + typealias RawValue = Int + case unrestricted // = 0 + case showLyrics // = 1 + case showLyricsMonthlyLimit // = 2 + case monthlyLimitReached // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .unrestricted + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unrestricted + case 1: self = .showLyrics + case 2: self = .showLyricsMonthlyLimit + case 3: self = .monthlyLimitReached + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unrestricted: return 0 + case .showLyrics: return 1 + case .showLyricsMonthlyLimit: return 2 + case .monthlyLimitReached: return 3 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension LyricsRestriction: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [LyricsRestriction] = [ + .unrestricted, + .showLyrics, + .showLyricsMonthlyLimit, + .monthlyLimitReached, + ] +} + +#endif // swift(>=4.2) + +struct LyricsLine { + // 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 offsetMs: Int32 = 0 + + var content: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct LyricsColors { + // 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 backgroundColor: UInt32 = 0 + + var lineColor: UInt32 = 0 + + var activeLineColor: UInt32 = 0 + + 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 + // methods supported on all messages. + + var timeSynchronized: Bool = false + + var lines: [LyricsLine] = [] + + var providedBy: String = String() + + var restriction: LyricsRestriction = .unrestricted + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Lyrics { + // 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 data: LyricsData { + get {return _data ?? LyricsData()} + set {_data = newValue} + } + /// Returns true if `data` has been explicitly set. + var hasData: Bool {return self._data != nil} + /// Clears the value of `data`. Subsequent reads from it will return its default value. + mutating func clearData() {self._data = nil} + + var colors: LyricsColors { + get {return _colors ?? LyricsColors()} + set {_colors = newValue} + } + /// Returns true if `colors` has been explicitly set. + var hasColors: Bool {return self._colors != nil} + /// Clears the value of `colors`. Subsequent reads from it will return its default value. + mutating func clearColors() {self._colors = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _data: LyricsData? = nil + fileprivate var _colors: LyricsColors? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension LyricsRestriction: @unchecked Sendable {} +extension LyricsLine: @unchecked Sendable {} +extension LyricsColors: @unchecked Sendable {} +extension LyricsData: @unchecked Sendable {} +extension Lyrics: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension LyricsRestriction: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNRESTRICTED"), + 1: .same(proto: "SHOW_LYRICS"), + 2: .same(proto: "SHOW_LYRICS_MONTHLY_LIMIT"), + 3: .same(proto: "MONTHLY_LIMIT_REACHED"), + ] +} + +extension LyricsLine: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "LyricsLine" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "offsetMs"), + 2: .same(proto: "content"), + ] + + 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.decodeSingularInt32Field(value: &self.offsetMs) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.content) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.offsetMs != 0 { + try visitor.visitSingularInt32Field(value: self.offsetMs, fieldNumber: 1) + } + if !self.content.isEmpty { + try visitor.visitSingularStringField(value: self.content, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: LyricsLine, rhs: LyricsLine) -> Bool { + if lhs.offsetMs != rhs.offsetMs {return false} + if lhs.content != rhs.content {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension LyricsColors: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "LyricsColors" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "backgroundColor"), + 2: .same(proto: "lineColor"), + 3: .same(proto: "activeLineColor"), + ] + + 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.decodeSingularUInt32Field(value: &self.backgroundColor) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.lineColor) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.activeLineColor) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.backgroundColor != 0 { + try visitor.visitSingularUInt32Field(value: self.backgroundColor, fieldNumber: 1) + } + if self.lineColor != 0 { + try visitor.visitSingularUInt32Field(value: self.lineColor, fieldNumber: 2) + } + if self.activeLineColor != 0 { + try visitor.visitSingularUInt32Field(value: self.activeLineColor, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: LyricsColors, rhs: LyricsColors) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor {return false} + if lhs.lineColor != rhs.lineColor {return false} + if lhs.activeLineColor != rhs.activeLineColor {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"), + ] + + 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.decodeSingularBoolField(value: &self.timeSynchronized) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.lines) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.providedBy) }() + case 14: try { try decoder.decodeSingularEnumField(value: &self.restriction) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.timeSynchronized != false { + try visitor.visitSingularBoolField(value: self.timeSynchronized, fieldNumber: 1) + } + if !self.lines.isEmpty { + try visitor.visitRepeatedMessageField(value: self.lines, fieldNumber: 2) + } + if !self.providedBy.isEmpty { + try visitor.visitSingularStringField(value: self.providedBy, fieldNumber: 5) + } + if self.restriction != .unrestricted { + try visitor.visitSingularEnumField(value: self.restriction, fieldNumber: 14) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: LyricsData, rhs: LyricsData) -> Bool { + if lhs.timeSynchronized != rhs.timeSynchronized {return false} + if lhs.lines != rhs.lines {return false} + if lhs.providedBy != rhs.providedBy {return false} + if lhs.restriction != rhs.restriction {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Lyrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "Lyrics" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "data"), + 2: .same(proto: "colors"), + ] + + 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.decodeSingularMessageField(value: &self._data) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._colors) }() + default: break + } + } + } + + 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 + try { if let v = self._data { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._colors { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Lyrics, rhs: Lyrics) -> Bool { + if lhs._data != rhs._data {return false} + if lhs._colors != rhs._colors {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/control b/control index 12b58a2..ea84e43 100644 --- a/control +++ b/control @@ -6,4 +6,4 @@ Description: A tweak to get Spotify Premium for free, just like Spotilife Maintainer: Eevee Author: Eevee Section: Tweaks -Depends: ${ORION}, firmware (>= 14.0) +Depends: ${ORION}, firmware (>= 14.0), org.swift.protobuf.SwiftProtobuf