diff --git a/.gitignore b/.gitignore index f24fe3f9..a09d2279 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ xcuserdata/ ._* .theos +.swiftpm /packages .theos/ packages/ diff --git a/Sources/EeveeSpotify/CustomLyrics.x.swift b/Sources/EeveeSpotify/CustomLyrics.x.swift deleted file mode 100644 index bc768395..00000000 --- a/Sources/EeveeSpotify/CustomLyrics.x.swift +++ /dev/null @@ -1,215 +0,0 @@ -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", - UserDefaults.lyricsSource != .musixmatch { - target.isEnabled = false - } - - return orig.intrinsicContentSize() - } -} - - -class ProfileSettingsSectionHook: ClassHook { - - static let targetName = "ProfileSettingsSection" - - func numberOfRows() -> Int { - return 2 - } - - func didSelectRow(_ row: Int) { - - if row == 1 { - - let rootSettingsController = WindowHelper.shared.findFirstViewController( - "RootSettingsViewController" - )! - - let eeveeSettingsController = EeveeSettingsViewController() - eeveeSettingsController.title = "EeveeSpotify" - - rootSettingsController.navigationController!.pushViewController( - eeveeSettingsController, - animated: true - ) - - return - } - - orig.didSelectRow(row) - } - - func cellForRow(_ row: Int) -> UITableViewCell { - - if row == 1 { - - let settingsTableCell = Dynamic.SPTSettingsTableViewCell - .alloc(interface: SPTSettingsTableViewCell.self) - .initWithStyle(3, reuseIdentifier: "EeveeSpotify") - - let tableViewCell = Dynamic.convert(settingsTableCell, to: UITableViewCell.self) - - tableViewCell.accessoryView = type( - of: Dynamic.SPTDisclosureAccessoryView - .alloc(interface: SPTDisclosureAccessoryView.self) - ) - .disclosureAccessoryView() - - tableViewCell.textLabel?.text = "EeveeSpotify" - return tableViewCell - } - - return orig.cellForRow(row) - } -} - -func getCurrentTrackLyricsData() throws -> Data { - - guard let track = HookedInstances.currentTrack else { - throw LyricsError.NoCurrentTrack - } - - var source = UserDefaults.lyricsSource - - let plainLyrics: PlainLyrics? - - do { - plainLyrics = try LyricsRepository.getLyrics( - title: track.trackTitle(), - artist: track.artistTitle(), - spotifyTrackId: track.URI().spt_trackIdentifier(), - source: source - ) - } - - catch { - - if source != .genius && UserDefaults.geniusFallback { - - NSLog("[EeveeSpotify] Unable to load lyrics from \(source): \(error), trying Genius as fallback") - source = .genius - - plainLyrics = try LyricsRepository.getLyrics( - title: track.trackTitle(), - artist: track.artistTitle(), - spotifyTrackId: track.URI().spt_trackIdentifier(), - source: source - ) - } - else { - throw error - } - } - - let lyrics = try Lyrics.with { - $0.colors = LyricsColors.with { - $0.backgroundColor = Color(hex: track.extractedColorHex()).normalized.uInt32 - $0.lineColor = Color.black.uInt32 - $0.activeLineColor = Color.white.uInt32 - } - $0.data = try LyricsHelper.composeLyricsData(plainLyrics!, source: source) - } - - 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/DataLoaderServiceHooks.x.swift b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift new file mode 100644 index 00000000..c39314be --- /dev/null +++ b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift @@ -0,0 +1,135 @@ +import Foundation +import Orion + +class SPTDataLoaderServiceHook: ClassHook { + + static let targetName = "SPTDataLoaderService" + + func URLSession( + _ session: URLSession, + task: URLSessionDataTask, + didCompleteWithError error: Error? + ) { + if let url = task.currentRequest?.url { + if url.isLyrics || (UserDefaults.patchType == .requests && url.isCustomize) { + return + } + } + orig.URLSession(session, task: task, didCompleteWithError: error) + } + + 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) + orig.URLSession(session, task: task, didCompleteWithError: nil) + + return + } + catch { + NSLog("[EeveeSpotify] Unable to load lyrics: \(error)") + orig.URLSession(session, task: task, didCompleteWithError: error) + + return + } + } + + orig.URLSession( + session, + dataTask: task, + didReceiveResponse: response, + completionHandler: handler + ) + } + + func URLSession( + _ session: URLSession, + dataTask task: URLSessionDataTask, + didReceiveData data: Data + ) { + guard + let request = task.currentRequest, + let response = task.response, + let url = request.url + else { + return + } + + if url.isLyrics { + + do { + orig.URLSession( + session, + dataTask: task, + didReceiveData: try getCurrentTrackLyricsData() + ) + + orig.URLSession(session, task: task, didCompleteWithError: nil) + return + } + catch { + NSLog("[EeveeSpotify] Unable to load lyrics: \(error)") + orig.URLSession(session, task: task, didCompleteWithError: error) + + return + } + } + + if url.isCustomize && UserDefaults.patchType == .requests { + + do { + guard let buffer = OfflineHelper.appendDataAndReturnIfFull( + data, + response: response + ) else { + return + } + + OfflineHelper.dataBuffer = Data() + + var customizeMessage = try CustomizeMessage(serializedData: buffer) + modifyAttributes(&customizeMessage.response.attributes.accountAttributes) + + orig.URLSession( + session, + dataTask: task, + didReceiveData: try customizeMessage.serializedData() + ) + + orig.URLSession(session, task: task, didCompleteWithError: nil) + + NSLog("[EeveeSpotify] Modified customize data") + return + } + catch { + NSLog("[EeveeSpotify] Unable to modify customize data: \(error)") + } + } + + orig.URLSession(session, dataTask: task, didReceiveData: data) + } +} diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift new file mode 100644 index 00000000..e81c025a --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift @@ -0,0 +1,79 @@ +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", + UserDefaults.lyricsSource != .musixmatch { + target.isEnabled = false + } + + return orig.intrinsicContentSize() + } +} + +func getCurrentTrackLyricsData() throws -> Data { + + guard let track = HookedInstances.currentTrack else { + throw LyricsError.NoCurrentTrack + } + + var source = UserDefaults.lyricsSource + + let plainLyrics: PlainLyrics? + + do { + plainLyrics = try LyricsRepository.getLyrics( + title: track.trackTitle(), + artist: track.artistTitle(), + spotifyTrackId: track.URI().spt_trackIdentifier(), + source: source + ) + } + + catch { + + if source != .genius && UserDefaults.geniusFallback { + + NSLog("[EeveeSpotify] Unable to load lyrics from \(source): \(error), trying Genius as fallback") + source = .genius + + plainLyrics = try LyricsRepository.getLyrics( + title: track.trackTitle(), + artist: track.artistTitle(), + spotifyTrackId: track.URI().spt_trackIdentifier(), + source: source + ) + } + else { + throw error + } + } + + let lyrics = try Lyrics.with { + $0.colors = LyricsColors.with { + $0.backgroundColor = Color(hex: track.extractedColorHex()).normalized.uInt32 + $0.lineColor = Color.black.uInt32 + $0.activeLineColor = Color.white.uInt32 + } + $0.data = try LyricsHelper.composeLyricsData(plainLyrics!, source: source) + } + + return try lyrics.serializedData() +} diff --git a/Sources/EeveeSpotify/Helpers/LyricsHelper.swift b/Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift similarity index 100% rename from Sources/EeveeSpotify/Helpers/LyricsHelper.swift rename to Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift diff --git a/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift index 0ef63022..58d35784 100644 --- a/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/URL+Extension.swift @@ -8,4 +8,12 @@ extension URL { var isOpenSpotifySafariExtension: Bool { self.host == "eevee" } -} \ No newline at end of file + + var isCustomize: Bool { + self.path.contains("v1/customize") + } + + var isBootstrap: Bool { + self.path.contains("v1/bootstrap") + } +} diff --git a/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift index 77963828..6d9630b7 100644 --- a/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift @@ -8,6 +8,7 @@ extension UserDefaults { private static let musixmatchTokenKey = "musixmatchToken" private static let geniusFallbackKey = "geniusFallback" private static let darkPopUpsKey = "darkPopUps" + private static let patchTypeKey = "patchType" static var lyricsSource: LyricsSource { get { @@ -48,4 +49,17 @@ extension UserDefaults { defaults.set(darkPopUps, forKey: darkPopUpsKey) } } -} \ No newline at end of file + + static var patchType: PatchType { + get { + if let rawValue = defaults.object(forKey: patchTypeKey) as? Int { + return PatchType(rawValue: rawValue)! + } + + return .notSet + } + set (patchType) { + defaults.set(patchType.rawValue, forKey: patchTypeKey) + } + } +} diff --git a/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift b/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift new file mode 100644 index 00000000..e73db92f --- /dev/null +++ b/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift @@ -0,0 +1,170 @@ +import Orion + +func showHavePremiumPopUp() { + PopUpHelper.showPopUp( + delayed: true, + message: "It looks like you have an active Premium subscription, so the tweak won't patch the data or restrict the use of Premium server-sided features. You can manage this in the EeveeSpotify settings.", + buttonText: "OK" + ) +} + +func showOfflineBnkMethodSetPopUp() { + PopUpHelper.showPopUp( + delayed: true, + message: "App restart is needed to get Premium. You can manage the Premium patching method in the EeveeSpotify settings.", + buttonText: "Restart Now", + secondButtonText: "Restart Later", + onPrimaryClick: { + exitApplication() + } + ) +} + +func modifyAttributes(_ attributes: inout [String: AccountAttribute]) { + + attributes["type"] = AccountAttribute.with { + $0.stringValue = "premium" + } + attributes["player-license"] = AccountAttribute.with { + $0.stringValue = "premium" + } + attributes["financial-product"] = AccountAttribute.with { + $0.stringValue = "pr:premium,tc:0" + } + attributes["name"] = AccountAttribute.with { + $0.stringValue = "Spotify Premium" + } + + // + + attributes["unrestricted"] = AccountAttribute.with { + $0.boolValue = true + } + attributes["catalogue"] = AccountAttribute.with { + $0.stringValue = "premium" + } + attributes["streaming-rules"] = AccountAttribute.with { + $0.stringValue = "" + } + attributes["pause-after"] = AccountAttribute.with { + $0.longValue = 0 + } + + // + + attributes["ads"] = AccountAttribute.with { + $0.boolValue = false + } + + attributes.removeValue(forKey: "ad-use-adlogic") + attributes.removeValue(forKey: "ad-catalogues") + + // + + attributes["shuffle-eligible"] = AccountAttribute.with { + $0.boolValue = true + } + attributes["high-bitrate"] = AccountAttribute.with { + $0.boolValue = true + } + attributes["offline"] = AccountAttribute.with { + $0.boolValue = true + } + attributes["nft-disabled"] = AccountAttribute.with { + $0.stringValue = "1" + } + attributes["can_use_superbird"] = AccountAttribute.with { + $0.boolValue = true + } + + // + + attributes["com.spotify.madprops.use.ucs.product.state"] = AccountAttribute.with { + $0.boolValue = true + } +} + +class SPTCoreURLSessionDataDelegateHook: ClassHook { + + static let targetName = "SPTCoreURLSessionDataDelegate" + + func URLSession( + _ session: URLSession, + task: URLSessionDataTask, + didCompleteWithError error: Error? + ) { + if let url = task.currentRequest?.url, UserDefaults.patchType == .requests && url.isBootstrap { + return + } + orig.URLSession(session, task: task, didCompleteWithError: error) + } + + func URLSession( + _ session: URLSession, + dataTask task: URLSessionDataTask, + didReceiveData data: Data + ) { + guard + let request = task.currentRequest, + let response = task.response, + let url = request.url + else { + return + } + + if url.isBootstrap { + + do { + guard let buffer = OfflineHelper.appendDataAndReturnIfFull( + data, + response: response + ) else { + return + } + + OfflineHelper.dataBuffer = Data() + + var bootstrapMessage = try BootstrapMessage(serializedData: buffer) + + if UserDefaults.patchType == .requests { + + modifyAttributes(&bootstrapMessage.attributes) + + orig.URLSession( + session, + dataTask: task, + didReceiveData: try bootstrapMessage.serializedData() + ) + + NSLog("[EeveeSpotify] Modified bootstrap data") + } + else { + + if UserDefaults.patchType == .notSet { + + if bootstrapMessage.attributes["type"]?.stringValue == "premium" { + UserDefaults.patchType = .disabled + showHavePremiumPopUp() + } + else { + UserDefaults.patchType = .offlineBnk + showOfflineBnkMethodSetPopUp() + } + + NSLog("[EeveeSpotify] Fetched bootstrap, \(UserDefaults.patchType) was set") + } + + orig.URLSession(session, dataTask: task, didReceiveData: buffer) + } + + orig.URLSession(session, task: task, didCompleteWithError: nil) + return + } + catch { + NSLog("[EeveeSpotify] Unable to modify bootstrap data: \(error)") + } + } + + orig.URLSession(session, dataTask: task, didReceiveData: data) + } +} diff --git a/Sources/EeveeSpotify/Helpers/BundleHelper.swift b/Sources/EeveeSpotify/Premium/Helpers/BundleHelper.swift similarity index 100% rename from Sources/EeveeSpotify/Helpers/BundleHelper.swift rename to Sources/EeveeSpotify/Premium/Helpers/BundleHelper.swift diff --git a/Sources/EeveeSpotify/Helpers/OfflineHelper.swift b/Sources/EeveeSpotify/Premium/Helpers/OfflineHelper.swift similarity index 71% rename from Sources/EeveeSpotify/Helpers/OfflineHelper.swift rename to Sources/EeveeSpotify/Premium/Helpers/OfflineHelper.swift index 1c5213b2..78ec753e 100755 --- a/Sources/EeveeSpotify/Helpers/OfflineHelper.swift +++ b/Sources/EeveeSpotify/Premium/Helpers/OfflineHelper.swift @@ -26,6 +26,21 @@ class OfflineHelper { get throws { try Data(contentsOf: eeveeBnkPath) } } + // + + static var dataBuffer = Data() + + static func appendDataAndReturnIfFull(_ data: Data, response: URLResponse) -> Data? { + + dataBuffer.append(data) + + if dataBuffer.count == response.expectedContentLength { + return dataBuffer + } + + return nil + } + // private static func writeOfflineBnkData(_ data: Data) throws { @@ -60,4 +75,14 @@ class OfflineHelper { try writeOfflineBnkData(blankData) } + + // + + static func resetPersistentCache() throws { + try FileManager.default.removeItem(at: self.persistentCachePath) + } + + static func resetOfflineBnk() throws { + try FileManager.default.removeItem(at: self.offlineBnkPath) + } } diff --git a/Sources/EeveeSpotify/Premium/Models/Account.pb.swift b/Sources/EeveeSpotify/Premium/Models/Account.pb.swift new file mode 100644 index 00000000..683cd079 --- /dev/null +++ b/Sources/EeveeSpotify/Premium/Models/Account.pb.swift @@ -0,0 +1,1333 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: account.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 +} + +struct SpotifyError { + // 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 errorCode: Int32 = 0 + + var errorMessage: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct CustomizeMessage { + // 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 result: CustomizeMessage.OneOf_Result? = nil + + var response: UcsResponse { + get { + if case .response(let v)? = result {return v} + return UcsResponse() + } + set {result = .response(newValue)} + } + + var error: SpotifyError { + get { + if case .error(let v)? = result {return v} + return SpotifyError() + } + set {result = .error(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Result: Equatable { + case response(UcsResponse) + case error(SpotifyError) + + #if !swift(>=4.1) + static func ==(lhs: CustomizeMessage.OneOf_Result, rhs: CustomizeMessage.OneOf_Result) -> Bool { + // 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 (lhs, rhs) { + case (.response, .response): return { + guard case .response(let l) = lhs, case .response(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.error, .error): return { + guard case .error(let l) = lhs, case .error(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct UcsResponse { + // 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 resolveResult: UcsResponse.OneOf_ResolveResult? = nil + + var resolve: ResolveResponse { + get { + if case .resolve(let v)? = resolveResult {return v} + return ResolveResponse() + } + set {resolveResult = .resolve(newValue)} + } + + var resolveError: SpotifyError { + get { + if case .resolveError(let v)? = resolveResult {return v} + return SpotifyError() + } + set {resolveResult = .resolveError(newValue)} + } + + var accountAttributesResult: UcsResponse.OneOf_AccountAttributesResult? = nil + + var attributes: AccountAttributesResponse { + get { + if case .attributes(let v)? = accountAttributesResult {return v} + return AccountAttributesResponse() + } + set {accountAttributesResult = .attributes(newValue)} + } + + var attributesError: SpotifyError { + get { + if case .attributesError(let v)? = accountAttributesResult {return v} + return SpotifyError() + } + set {accountAttributesResult = .attributesError(newValue)} + } + + var fetchTimeMillis: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_ResolveResult: Equatable { + case resolve(ResolveResponse) + case resolveError(SpotifyError) + + #if !swift(>=4.1) + static func ==(lhs: UcsResponse.OneOf_ResolveResult, rhs: UcsResponse.OneOf_ResolveResult) -> Bool { + // 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 (lhs, rhs) { + case (.resolve, .resolve): return { + guard case .resolve(let l) = lhs, case .resolve(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.resolveError, .resolveError): return { + guard case .resolveError(let l) = lhs, case .resolveError(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + enum OneOf_AccountAttributesResult: Equatable { + case attributes(AccountAttributesResponse) + case attributesError(SpotifyError) + + #if !swift(>=4.1) + static func ==(lhs: UcsResponse.OneOf_AccountAttributesResult, rhs: UcsResponse.OneOf_AccountAttributesResult) -> Bool { + // 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 (lhs, rhs) { + case (.attributes, .attributes): return { + guard case .attributes(let l) = lhs, case .attributes(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.attributesError, .attributesError): return { + guard case .attributesError(let l) = lhs, case .attributesError(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct ResolveResponse { + // 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 configuration: ResolveConfiguration { + get {return _configuration ?? ResolveConfiguration()} + set {_configuration = newValue} + } + /// Returns true if `configuration` has been explicitly set. + var hasConfiguration: Bool {return self._configuration != nil} + /// Clears the value of `configuration`. Subsequent reads from it will return its default value. + mutating func clearConfiguration() {self._configuration = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _configuration: ResolveConfiguration? = nil +} + +struct ResolveConfiguration { + // 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 configurationAssignmentID: String = String() + + var fetchTimeMillis: Int64 = 0 + + var assignedValues: [AssignedValue] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct AssignedValue { + // 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 propertyID: AssignedIdentifier { + get {return _propertyID ?? AssignedIdentifier()} + set {_propertyID = newValue} + } + /// Returns true if `propertyID` has been explicitly set. + var hasPropertyID: Bool {return self._propertyID != nil} + /// Clears the value of `propertyID`. Subsequent reads from it will return its default value. + mutating func clearPropertyID() {self._propertyID = nil} + + var metadata: AssignedMetadata { + get {return _metadata ?? AssignedMetadata()} + set {_metadata = newValue} + } + /// Returns true if `metadata` has been explicitly set. + var hasMetadata: Bool {return self._metadata != nil} + /// Clears the value of `metadata`. Subsequent reads from it will return its default value. + mutating func clearMetadata() {self._metadata = nil} + + var structuredValue: AssignedValue.OneOf_StructuredValue? = nil + + var boolValue: BoolValue { + get { + if case .boolValue(let v)? = structuredValue {return v} + return BoolValue() + } + set {structuredValue = .boolValue(newValue)} + } + + var intValue: IntValue { + get { + if case .intValue(let v)? = structuredValue {return v} + return IntValue() + } + set {structuredValue = .intValue(newValue)} + } + + var enumValue: EnumValue { + get { + if case .enumValue(let v)? = structuredValue {return v} + return EnumValue() + } + set {structuredValue = .enumValue(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_StructuredValue: Equatable { + case boolValue(BoolValue) + case intValue(IntValue) + case enumValue(EnumValue) + + #if !swift(>=4.1) + static func ==(lhs: AssignedValue.OneOf_StructuredValue, rhs: AssignedValue.OneOf_StructuredValue) -> Bool { + // 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 (lhs, rhs) { + case (.boolValue, .boolValue): return { + guard case .boolValue(let l) = lhs, case .boolValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.intValue, .intValue): return { + guard case .intValue(let l) = lhs, case .intValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.enumValue, .enumValue): return { + guard case .enumValue(let l) = lhs, case .enumValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} + + fileprivate var _propertyID: AssignedIdentifier? = nil + fileprivate var _metadata: AssignedMetadata? = nil +} + +struct AssignedIdentifier { + // 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 scope: String = String() + + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct AssignedMetadata { + // 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 policyID: Int64 = 0 + + var externalRealm: String = String() + + var externalRealmID: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct BoolValue { + // 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 value: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct IntValue { + // 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 value: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct EnumValue { + // 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 value: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct AccountAttributesResponse { + // 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 accountAttributes: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct AccountAttribute { + // 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 value: AccountAttribute.OneOf_Value? = nil + + var boolValue: Bool { + get { + if case .boolValue(let v)? = value {return v} + return false + } + set {value = .boolValue(newValue)} + } + + var longValue: Int64 { + get { + if case .longValue(let v)? = value {return v} + return 0 + } + set {value = .longValue(newValue)} + } + + var stringValue: String { + get { + if case .stringValue(let v)? = value {return v} + return String() + } + set {value = .stringValue(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Value: Equatable { + case boolValue(Bool) + case longValue(Int64) + case stringValue(String) + + #if !swift(>=4.1) + static func ==(lhs: AccountAttribute.OneOf_Value, rhs: AccountAttribute.OneOf_Value) -> Bool { + // 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 (lhs, rhs) { + case (.boolValue, .boolValue): return { + guard case .boolValue(let l) = lhs, case .boolValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.longValue, .longValue): return { + guard case .longValue(let l) = lhs, case .longValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.stringValue, .stringValue): return { + guard case .stringValue(let l) = lhs, case .stringValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct BootstrapMessage { + // 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 wrapper: CustomizationMessageWrapper { + get {return _wrapper ?? CustomizationMessageWrapper()} + set {_wrapper = newValue} + } + /// Returns true if `wrapper` has been explicitly set. + var hasWrapper: Bool {return self._wrapper != nil} + /// Clears the value of `wrapper`. Subsequent reads from it will return its default value. + mutating func clearWrapper() {self._wrapper = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _wrapper: CustomizationMessageWrapper? = nil +} + +struct CustomizationMessageWrapper { + // 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 oneMoreWrapper: CustomizationMessageOneMoreWrapper { + get {return _oneMoreWrapper ?? CustomizationMessageOneMoreWrapper()} + set {_oneMoreWrapper = newValue} + } + /// Returns true if `oneMoreWrapper` has been explicitly set. + var hasOneMoreWrapper: Bool {return self._oneMoreWrapper != nil} + /// Clears the value of `oneMoreWrapper`. Subsequent reads from it will return its default value. + mutating func clearOneMoreWrapper() {self._oneMoreWrapper = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _oneMoreWrapper: CustomizationMessageOneMoreWrapper? = nil +} + +struct CustomizationMessageOneMoreWrapper { + // 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 message: CustomizeMessage { + get {return _message ?? CustomizeMessage()} + set {_message = newValue} + } + /// Returns true if `message` has been explicitly set. + var hasMessage: Bool {return self._message != nil} + /// Clears the value of `message`. Subsequent reads from it will return its default value. + mutating func clearMessage() {self._message = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _message: CustomizeMessage? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension SpotifyError: @unchecked Sendable {} +extension CustomizeMessage: @unchecked Sendable {} +extension CustomizeMessage.OneOf_Result: @unchecked Sendable {} +extension UcsResponse: @unchecked Sendable {} +extension UcsResponse.OneOf_ResolveResult: @unchecked Sendable {} +extension UcsResponse.OneOf_AccountAttributesResult: @unchecked Sendable {} +extension ResolveResponse: @unchecked Sendable {} +extension ResolveConfiguration: @unchecked Sendable {} +extension AssignedValue: @unchecked Sendable {} +extension AssignedValue.OneOf_StructuredValue: @unchecked Sendable {} +extension AssignedIdentifier: @unchecked Sendable {} +extension AssignedMetadata: @unchecked Sendable {} +extension BoolValue: @unchecked Sendable {} +extension IntValue: @unchecked Sendable {} +extension EnumValue: @unchecked Sendable {} +extension AccountAttributesResponse: @unchecked Sendable {} +extension AccountAttribute: @unchecked Sendable {} +extension AccountAttribute.OneOf_Value: @unchecked Sendable {} +extension BootstrapMessage: @unchecked Sendable {} +extension CustomizationMessageWrapper: @unchecked Sendable {} +extension CustomizationMessageOneMoreWrapper: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension SpotifyError: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "SpotifyError" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "errorCode"), + 2: .same(proto: "errorMessage"), + ] + + 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.errorCode) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.errorCode != 0 { + try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SpotifyError, rhs: SpotifyError) -> Bool { + if lhs.errorCode != rhs.errorCode {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension CustomizeMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "CustomizeMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "response"), + 2: .same(proto: "error"), + ] + + 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 { + var v: UcsResponse? + var hadOneofValue = false + if let current = self.result { + hadOneofValue = true + if case .response(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.result = .response(v) + } + }() + case 2: try { + var v: SpotifyError? + var hadOneofValue = false + if let current = self.result { + hadOneofValue = true + if case .error(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.result = .error(v) + } + }() + 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 + switch self.result { + case .response?: try { + guard case .response(let v)? = self.result else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .error?: try { + guard case .error(let v)? = self.result else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: CustomizeMessage, rhs: CustomizeMessage) -> Bool { + if lhs.result != rhs.result {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension UcsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "UcsResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "resolve"), + 2: .same(proto: "resolveError"), + 3: .same(proto: "attributes"), + 4: .same(proto: "attributesError"), + 5: .same(proto: "fetchTimeMillis"), + ] + + 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 { + var v: ResolveResponse? + var hadOneofValue = false + if let current = self.resolveResult { + hadOneofValue = true + if case .resolve(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.resolveResult = .resolve(v) + } + }() + case 2: try { + var v: SpotifyError? + var hadOneofValue = false + if let current = self.resolveResult { + hadOneofValue = true + if case .resolveError(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.resolveResult = .resolveError(v) + } + }() + case 3: try { + var v: AccountAttributesResponse? + var hadOneofValue = false + if let current = self.accountAttributesResult { + hadOneofValue = true + if case .attributes(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.accountAttributesResult = .attributes(v) + } + }() + case 4: try { + var v: SpotifyError? + var hadOneofValue = false + if let current = self.accountAttributesResult { + hadOneofValue = true + if case .attributesError(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.accountAttributesResult = .attributesError(v) + } + }() + case 5: try { try decoder.decodeSingularInt64Field(value: &self.fetchTimeMillis) }() + 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 + switch self.resolveResult { + case .resolve?: try { + guard case .resolve(let v)? = self.resolveResult else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .resolveError?: try { + guard case .resolveError(let v)? = self.resolveResult else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + switch self.accountAttributesResult { + case .attributes?: try { + guard case .attributes(let v)? = self.accountAttributesResult else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .attributesError?: try { + guard case .attributesError(let v)? = self.accountAttributesResult else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + if self.fetchTimeMillis != 0 { + try visitor.visitSingularInt64Field(value: self.fetchTimeMillis, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: UcsResponse, rhs: UcsResponse) -> Bool { + if lhs.resolveResult != rhs.resolveResult {return false} + if lhs.accountAttributesResult != rhs.accountAttributesResult {return false} + if lhs.fetchTimeMillis != rhs.fetchTimeMillis {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ResolveResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "ResolveResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "configuration"), + ] + + 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._configuration) }() + 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._configuration { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ResolveResponse, rhs: ResolveResponse) -> Bool { + if lhs._configuration != rhs._configuration {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ResolveConfiguration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "ResolveConfiguration" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "configurationAssignmentId"), + 2: .same(proto: "fetchTimeMillis"), + 3: .same(proto: "assignedValues"), + ] + + 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.configurationAssignmentID) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self.fetchTimeMillis) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.assignedValues) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.configurationAssignmentID.isEmpty { + try visitor.visitSingularStringField(value: self.configurationAssignmentID, fieldNumber: 1) + } + if self.fetchTimeMillis != 0 { + try visitor.visitSingularInt64Field(value: self.fetchTimeMillis, fieldNumber: 2) + } + if !self.assignedValues.isEmpty { + try visitor.visitRepeatedMessageField(value: self.assignedValues, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ResolveConfiguration, rhs: ResolveConfiguration) -> Bool { + if lhs.configurationAssignmentID != rhs.configurationAssignmentID {return false} + if lhs.fetchTimeMillis != rhs.fetchTimeMillis {return false} + if lhs.assignedValues != rhs.assignedValues {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AssignedValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AssignedValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "propertyId"), + 2: .same(proto: "metadata"), + 3: .same(proto: "boolValue"), + 4: .same(proto: "intValue"), + 5: .same(proto: "enumValue"), + ] + + 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._propertyID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._metadata) }() + case 3: try { + var v: BoolValue? + var hadOneofValue = false + if let current = self.structuredValue { + hadOneofValue = true + if case .boolValue(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.structuredValue = .boolValue(v) + } + }() + case 4: try { + var v: IntValue? + var hadOneofValue = false + if let current = self.structuredValue { + hadOneofValue = true + if case .intValue(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.structuredValue = .intValue(v) + } + }() + case 5: try { + var v: EnumValue? + var hadOneofValue = false + if let current = self.structuredValue { + hadOneofValue = true + if case .enumValue(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.structuredValue = .enumValue(v) + } + }() + 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._propertyID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._metadata { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.structuredValue { + case .boolValue?: try { + guard case .boolValue(let v)? = self.structuredValue else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .intValue?: try { + guard case .intValue(let v)? = self.structuredValue else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .enumValue?: try { + guard case .enumValue(let v)? = self.structuredValue else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AssignedValue, rhs: AssignedValue) -> Bool { + if lhs._propertyID != rhs._propertyID {return false} + if lhs._metadata != rhs._metadata {return false} + if lhs.structuredValue != rhs.structuredValue {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AssignedIdentifier: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AssignedIdentifier" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "scope"), + 2: .same(proto: "name"), + ] + + 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.scope) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.scope.isEmpty { + try visitor.visitSingularStringField(value: self.scope, fieldNumber: 1) + } + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AssignedIdentifier, rhs: AssignedIdentifier) -> Bool { + if lhs.scope != rhs.scope {return false} + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AssignedMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AssignedMetadata" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "policyId"), + 2: .same(proto: "externalRealm"), + 3: .same(proto: "externalRealmId"), + ] + + 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.decodeSingularInt64Field(value: &self.policyID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.externalRealm) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &self.externalRealmID) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.policyID != 0 { + try visitor.visitSingularInt64Field(value: self.policyID, fieldNumber: 1) + } + if !self.externalRealm.isEmpty { + try visitor.visitSingularStringField(value: self.externalRealm, fieldNumber: 2) + } + if self.externalRealmID != 0 { + try visitor.visitSingularInt64Field(value: self.externalRealmID, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AssignedMetadata, rhs: AssignedMetadata) -> Bool { + if lhs.policyID != rhs.policyID {return false} + if lhs.externalRealm != rhs.externalRealm {return false} + if lhs.externalRealmID != rhs.externalRealmID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "BoolValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + 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.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.value != false { + try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: BoolValue, rhs: BoolValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension IntValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "IntValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + 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.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.value != 0 { + try visitor.visitSingularInt32Field(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: IntValue, rhs: IntValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension EnumValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "EnumValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + 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.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.value.isEmpty { + try visitor.visitSingularStringField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: EnumValue, rhs: EnumValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AccountAttributesResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AccountAttributesResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "accountAttributes"), + ] + + 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.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.accountAttributes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.accountAttributes.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.accountAttributes, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AccountAttributesResponse, rhs: AccountAttributesResponse) -> Bool { + if lhs.accountAttributes != rhs.accountAttributes {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AccountAttribute: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AccountAttribute" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 2: .same(proto: "boolValue"), + 3: .same(proto: "longValue"), + 4: .same(proto: "stringValue"), + ] + + 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 2: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .boolValue(v) + } + }() + case 3: try { + var v: Int64? + try decoder.decodeSingularInt64Field(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .longValue(v) + } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .stringValue(v) + } + }() + 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 + switch self.value { + case .boolValue?: try { + guard case .boolValue(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + }() + case .longValue?: try { + guard case .longValue(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularInt64Field(value: v, fieldNumber: 3) + }() + case .stringValue?: try { + guard case .stringValue(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AccountAttribute, rhs: AccountAttribute) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension BootstrapMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "BootstrapMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 2: .same(proto: "wrapper"), + ] + + 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 2: try { try decoder.decodeSingularMessageField(value: &self._wrapper) }() + 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._wrapper { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: BootstrapMessage, rhs: BootstrapMessage) -> Bool { + if lhs._wrapper != rhs._wrapper {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension CustomizationMessageWrapper: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "CustomizationMessageWrapper" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "oneMoreWrapper"), + ] + + 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._oneMoreWrapper) }() + 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._oneMoreWrapper { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: CustomizationMessageWrapper, rhs: CustomizationMessageWrapper) -> Bool { + if lhs._oneMoreWrapper != rhs._oneMoreWrapper {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension CustomizationMessageOneMoreWrapper: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "CustomizationMessageOneMoreWrapper" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "message"), + ] + + 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._message) }() + 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._message { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: CustomizationMessageOneMoreWrapper, rhs: CustomizationMessageOneMoreWrapper) -> Bool { + if lhs._message != rhs._message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift b/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift new file mode 100644 index 00000000..e7de3d71 --- /dev/null +++ b/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by eevee on 28/05/2024. +// + +import Foundation + +extension BootstrapMessage { + + var attributes: [String: AccountAttribute] { + get { + self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes + } + set(attributes) { + self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes = attributes + } + } +} diff --git a/Sources/EeveeSpotify/Premium/Models/PatchType.swift b/Sources/EeveeSpotify/Premium/Models/PatchType.swift new file mode 100644 index 00000000..2fdcbff3 --- /dev/null +++ b/Sources/EeveeSpotify/Premium/Models/PatchType.swift @@ -0,0 +1,12 @@ +import Foundation + +enum PatchType: Int { + case notSet + case disabled + case offlineBnk + case requests + + var isPatching: Bool { + self == .requests || self == .offlineBnk + } +} diff --git a/Sources/EeveeSpotify/OfflineObserver.swift b/Sources/EeveeSpotify/Premium/OfflineObserver.swift similarity index 67% rename from Sources/EeveeSpotify/OfflineObserver.swift rename to Sources/EeveeSpotify/Premium/OfflineObserver.swift index 2fc0d194..e7eead70 100644 --- a/Sources/EeveeSpotify/OfflineObserver.swift +++ b/Sources/EeveeSpotify/Premium/OfflineObserver.swift @@ -1,14 +1,6 @@ import Foundation import UIKit -func exitApplication() { - - UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) - Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in - exit(EXIT_SUCCESS) - } -} - class OfflineObserver: NSObject, NSFilePresenter { var presentedItemURL: URL? @@ -25,9 +17,9 @@ class OfflineObserver: NSObject, NSFilePresenter { if productState.stringForKey("type") == "premium" { - if productState.stringForKey("shuffle") == "0" { - return - } +// if productState.stringForKey("shuffle") == "0" { +// return +// } do { try OfflineHelper.backupToEeveeBnk() @@ -41,14 +33,14 @@ class OfflineObserver: NSObject, NSFilePresenter { } PopUpHelper.showPopUp( - message: "Spotify has just reloaded user data, and you've been switched to the Free plan. It's fine; simply restart the app, and the tweak will patch the data again. If this doesn't work, there might be a problem with the saved data. You can reset it and restart the app. Note: after resetting, you need to restart the app twice.", + message: "Spotify has just reloaded user data, and you've been switched to the Free plan. It's fine; simply restart the app, and the tweak will patch the data again. If this doesn't work, there might be a problem with the cached data. You can reset it and restart the app. Note: after resetting, you need to restart the app twice. You can also manage the Premium patching method in the EeveeSpotify settings.", buttonText: "Restart App", secondButtonText: "Reset Data and Restart App", - onPrimaryClick: { + onPrimaryClick: { exitApplication() }, onSecondaryClick: { - try! FileManager.default.removeItem(at: OfflineHelper.persistentCachePath) + try! OfflineHelper.resetPersistentCache() exitApplication() } ) diff --git a/Sources/EeveeSpotify/ServerSidedReminder.x.swift b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift similarity index 89% rename from Sources/EeveeSpotify/ServerSidedReminder.x.swift rename to Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift index e1edf3b1..bc059b48 100644 --- a/Sources/EeveeSpotify/ServerSidedReminder.x.swift +++ b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift @@ -1,8 +1,11 @@ import Orion import UIKit +struct ServerSidedReminder: HookGroup { } + class StreamQualitySettingsSectionHook: ClassHook { + typealias Group = ServerSidedReminder static let targetName = "StreamQualitySettingsSection" func shouldResetSelection() -> Bool { @@ -26,7 +29,8 @@ func showOfflineModePopUp() { } class FTPDownloadActionHook: ClassHook { - + + typealias Group = ServerSidedReminder static let targetName = "ListUXPlatform_FreeTierPlaylistImpl.FTPDownloadAction" func execute(_ idk: Any) { @@ -36,6 +40,8 @@ class FTPDownloadActionHook: ClassHook { class UIButtonHook: ClassHook { + typealias Group = ServerSidedReminder + func setHighlighted(_ highlighted: Bool) { if highlighted { @@ -56,4 +62,4 @@ class UIButtonHook: ClassHook { orig.setHighlighted(highlighted) } -} \ No newline at end of file +} diff --git a/Sources/EeveeSpotify/Tweak.x.swift b/Sources/EeveeSpotify/Tweak.x.swift index d1c2a592..6b7be71b 100644 --- a/Sources/EeveeSpotify/Tweak.x.swift +++ b/Sources/EeveeSpotify/Tweak.x.swift @@ -1,4 +1,13 @@ import Orion +import UIKit + +func exitApplication() { + + UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) + Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in + exit(EXIT_SUCCESS) + } +} class URLHook: ClassHook { @@ -21,49 +30,124 @@ class URLHook: ClassHook { } } -struct EeveeSpotify: Tweak { - - static let version = "3.1" - - init() { +class ProfileSettingsSectionHook: ClassHook { - do { + static let targetName = "ProfileSettingsSection" - if UserDefaults.darkPopUps { - DarkPopUps().activate() - } + func numberOfRows() -> Int { + return 2 + } - defer { - NSFileCoordinator.addFilePresenter(OfflineObserver()) - } + func didSelectRow(_ row: Int) { - do { - try OfflineHelper.restoreFromEeveeBnk() - NSLog("[EeveeSpotify] Restored from eevee.bnk") + if row == 1 { - return - } + let rootSettingsController = WindowHelper.shared.findFirstViewController( + "RootSettingsViewController" + )! - catch CocoaError.fileReadNoSuchFile { - NSLog("[EeveeSpotify] Not restoring from eevee.bnk: doesn't exist") - } + let eeveeSettingsController = EeveeSettingsViewController() + eeveeSettingsController.title = "EeveeSpotify" + + rootSettingsController.navigationController!.pushViewController( + eeveeSettingsController, + animated: true + ) - // + return + } - do { - try OfflineHelper.patchOfflineBnk() - try OfflineHelper.backupToEeveeBnk() - } + orig.didSelectRow(row) + } + + func cellForRow(_ row: Int) -> UITableViewCell { + + if row == 1 { - catch CocoaError.fileReadNoSuchFile { + let settingsTableCell = Dynamic.SPTSettingsTableViewCell + .alloc(interface: SPTSettingsTableViewCell.self) + .initWithStyle(3, reuseIdentifier: "EeveeSpotify") + + let tableViewCell = Dynamic.convert(settingsTableCell, to: UITableViewCell.self) + + tableViewCell.accessoryView = type( + of: Dynamic.SPTDisclosureAccessoryView + .alloc(interface: SPTDisclosureAccessoryView.self) + ) + .disclosureAccessoryView() + + tableViewCell.textLabel?.text = "EeveeSpotify" + return tableViewCell + } + + return orig.cellForRow(row) + } +} + +struct EeveeSpotify: Tweak { + + static let version = "4.0" + + init() { + + do { - NSLog("[EeveeSpotify] Not activating: offline.bnk doesn't exist") + defer { - PopUpHelper.showPopUp( - delayed: true, - message: "Please log in and restart the app to get Premium.", - buttonText: "Okay!" - ) + if UserDefaults.darkPopUps { + DarkPopUps().activate() + } + + let patchType = UserDefaults.patchType + + if patchType.isPatching { + + if patchType == .offlineBnk { + NSFileCoordinator.addFilePresenter(OfflineObserver()) + } + + ServerSidedReminder().activate() + } + } + + switch UserDefaults.patchType { + + case .disabled: + + NSLog("[EeveeSpotify] Not activating: patchType is disabled") + return + + case .offlineBnk: + + do { + try OfflineHelper.restoreFromEeveeBnk() + + NSLog("[EeveeSpotify] Restored from eevee.bnk") + return + } + + catch CocoaError.fileReadNoSuchFile { + NSLog("[EeveeSpotify] Not restoring from eevee.bnk: doesn't exist") + } + + do { + try OfflineHelper.patchOfflineBnk() + try OfflineHelper.backupToEeveeBnk() + } + + catch CocoaError.fileReadNoSuchFile { + + NSLog("[EeveeSpotify] Not activating: offline.bnk doesn't exist") + + PopUpHelper.showPopUp( + delayed: true, + message: "Please log in and restart the app to get Premium.", + buttonText: "OK" + ) + } + + default: + break } } diff --git a/Sources/EeveeSpotify/Views/EeveeSettingsView.swift b/Sources/EeveeSpotify/Views/EeveeSettingsView.swift index 8b0572e6..6b6595ed 100644 --- a/Sources/EeveeSpotify/Views/EeveeSettingsView.swift +++ b/Sources/EeveeSpotify/Views/EeveeSettingsView.swift @@ -4,6 +4,7 @@ import UIKit struct EeveeSettingsView: View { @State private var musixmatchToken = UserDefaults.musixmatchToken + @State private var patchType = UserDefaults.patchType @State private var lyricsSource = UserDefaults.lyricsSource private func showMusixmatchTokenAlert(_ oldSource: LyricsSource) { @@ -48,6 +49,34 @@ struct EeveeSettingsView: View { var body: some View { List { + + Section(footer: patchType == .disabled ? nil : Text(""" +You can select the Premium patching method you prefer. App restart is required after changing. + +Static: The original method. On app start, the tweak composes cache data by inserting your username into a blank file with preset Premium parameters. When Spotify reloads user data, you'll be switched to the Free plan and see a popup with quick restart app and reset data actions. + +Dynamic: This method intercepts requests to load user data, deserializes it, and modifies the parameters in real-time. It's much more stable and is recommended. + +If you have an active Premium subscription, you can turn on Do Not Patch Premium. The tweak won't patch the data or restrict the use of Premium server-sided features. +""")) { + Toggle( + "Do Not Patch Premium", + isOn: Binding( + get: { patchType == .disabled }, + set: { patchType = $0 ? .disabled : .offlineBnk } + ) + ) + if patchType != .disabled { + Picker( + "Patching Method", + selection: $patchType + ) { + Text("Static").tag(PatchType.offlineBnk) + Text("Dynamic").tag(PatchType.requests) + } + } + } + Section(footer: Text(""" You can select the lyrics source you prefer. @@ -57,7 +86,7 @@ LRCLIB: The most open service, offering time-synced lyrics. However, it lacks ly Musixmatch: The service Spotify uses. Provides time-synced lyrics for many songs, but you'll need a user token to use this source. -If the tweak is unable to find a song or process the lyrics, you'll see the original Spotify one or a "Couldn't load the lyrics for this song" message. The lyrics might be wrong for some songs (e.g. another song, song article) when using Genius due to how the tweak searches songs. I've made it work in most cases. +If the tweak is unable to find a song or process the lyrics, you'll see a "Couldn't load the lyrics for this song" message. The lyrics might be wrong for some songs (e.g. another song, song article) when using Genius due to how the tweak searches songs. I've made it work in most cases. """)) { Picker( "Lyrics Source", @@ -95,9 +124,7 @@ If the tweak is unable to find a song or process the lyrics, you'll see the orig } } - Section( - footer: Text("App restart is required to apply.") - ) { + Section { Toggle( "Dark PopUps", isOn: Binding( @@ -106,11 +133,21 @@ If the tweak is unable to find a song or process the lyrics, you'll see the orig ) ) } + + Section(footer: Text("Clear cached data and restart the app.")) { + Button { + try! OfflineHelper.resetPersistentCache() + exitApplication() + } label: { + Text("Reset Data") + } + } } .padding(.bottom, 40) - + .animation(.default, value: lyricsSource) + .animation(.default, value: patchType) .onChange(of: musixmatchToken) { token in UserDefaults.musixmatchToken = token @@ -125,6 +162,18 @@ If the tweak is unable to find a song or process the lyrics, you'll see the orig UserDefaults.lyricsSource = newSource } + + .onChange(of: patchType) { newPatchType in + + UserDefaults.patchType = newPatchType + + do { + try OfflineHelper.resetOfflineBnk() + } + catch { + NSLog("Unable to reset offline.bnk: \(error)") + } + } .listStyle(GroupedListStyle()) @@ -136,4 +185,4 @@ If the tweak is unable to find a song or process the lyrics, you'll see the orig WindowHelper.shared.overrideUserInterfaceStyle(.dark) } } -} \ No newline at end of file +} diff --git a/control b/control index 2202b8b6..24a4a570 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.eevee.spotify Name: EeveeSpotify -Version: 3.1 +Version: 4.0 Architecture: iphoneos-arm Description: A tweak to get Spotify Premium for free, just like Spotilife Maintainer: Eevee