premium plan info and improvements

This commit is contained in:
eevee
2025-08-02 21:10:54 +03:00
parent 3b281334c4
commit 8f7c2c03ad
18 changed files with 933 additions and 149 deletions

View File

@@ -6,11 +6,17 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
// orion:new
func shouldModify(_ url: URL) -> Bool {
let isModifyingCustomizeResponse = PremiumPatchingGroup.isActive
let isModifyingLyrics = LyricsGroup.isActive
let shouldPatchPremium = PremiumPatchingGroup.isActive
let shouldReplaceLyrics = LyricsGroup.isActive
return (url.isLyrics && isModifyingLyrics)
|| (url.isCustomize && isModifyingCustomizeResponse)
return (shouldReplaceLyrics && url.isLyrics)
|| (shouldPatchPremium && (url.isCustomize || url.isPremiumPlanRow || url.isPlanOverview))
}
// orion:new
func respondWithCustomData(_ data: Data, task: URLSessionDataTask, session: URLSession) {
orig.URLSession(session, dataTask: task, didReceiveData: data)
orig.URLSession(session, task: task, didCompleteWithError: nil)
}
func URLSession(
@@ -18,58 +24,65 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
task: URLSessionDataTask,
didCompleteWithError error: Error?
) {
guard
let request = task.currentRequest,
let url = request.url
else {
guard let url = task.currentRequest?.url else {
return
}
if error == nil,
shouldModify(url),
let buffer = URLSessionHelper.shared.obtainData(for: url)
{
if url.isLyrics {
do {
orig.URLSession(
session,
dataTask: task,
didReceiveData: try getLyricsDataForCurrentTrack(
guard error == nil, shouldModify(url) else {
orig.URLSession(session, task: task, didCompleteWithError: error)
return
}
do {
if let buffer = URLSessionHelper.shared.obtainData(for: url) {
if url.isLyrics {
respondWithCustomData(
try getLyricsDataForCurrentTrack(
originalLyrics: try? Lyrics(serializedBytes: buffer)
)
),
task: task,
session: session
)
return
}
if url.isPremiumPlanRow {
respondWithCustomData(
try getPremiumPlanRowData(
originalPremiumPlanRow: try PremiumPlanRow(serializedBytes: buffer)
),
task: task,
session: session
)
return
}
var customizeMessage = try CustomizeMessage(serializedBytes: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
respondWithCustomData(try customizeMessage.serializedData(), task: task, session: session)
return
}
if url.isPlanOverview {
do {
orig.URLSession(session, dataTask: task, didReceiveData: try getPlanOverviewData())
orig.URLSession(session, task: task, didCompleteWithError: nil)
}
catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
return
}
do {
var customizeMessage = try CustomizeMessage(serializedBytes: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
orig.URLSession(
session,
dataTask: task,
didReceiveData: try customizeMessage.serializedBytes()
)
orig.URLSession(session, task: task, didCompleteWithError: nil)
NSLog("[EeveeSpotify] Modified customize data")
return
}
catch {
NSLog("[EeveeSpotify] Unable to modify customize data: \(error)")
}
}
catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
orig.URLSession(session, task: task, didCompleteWithError: error)
}
func URLSession(
@@ -79,47 +92,23 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
) {
guard
let request = task.currentRequest,
let url = request.url
let url = task.currentRequest?.url,
url.isLyrics,
response.statusCode != 200
else {
orig.URLSession(session, dataTask: task, didReceiveResponse: response, completionHandler: handler)
return
}
if shouldModify(url), url.isLyrics, response.statusCode != 200 {
let okResponse = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: "2.0",
headerFields: [:]
)!
do {
let data = try getLyricsDataForCurrentTrack()
let okResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "2.0", headerFields: [:])!
do {
let lyricsData = try getLyricsDataForCurrentTrack()
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 {
orig.URLSession(session, task: task, didCompleteWithError: error)
return
}
orig.URLSession(session, dataTask: task, didReceiveResponse: okResponse, completionHandler: handler)
respondWithCustomData(data, task: task, session: session)
} catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
orig.URLSession(
session,
dataTask: task,
didReceiveResponse: response,
completionHandler: handler
)
}
func URLSession(
@@ -127,10 +116,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
dataTask task: URLSessionDataTask,
didReceiveData data: Data
) {
guard
let request = task.currentRequest,
let url = request.url
else {
guard let url = task.currentRequest?.url else {
return
}

View File

@@ -3,7 +3,9 @@ import UIKit
class SPTPlayerTrackHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static let targetName = "SPTPlayerTrack"
static let targetName = EeveeSpotify.hookTarget == .latest
? "SPTPlayerTrackImplementation"
: "SPTPlayerTrack"
func metadata() -> [String: String] {
var meta = orig.metadata()
@@ -31,7 +33,7 @@ class NowPlayingScrollViewControllerHook: ClassHook<NSObject> {
withDifferentProviders: Bool,
scrollEnabledValueChanged: Bool
) -> NowPlayingScrollViewController {
var controller = orig.nowPlayingScrollViewModelWithDidLoadComponentsFor(
let controller = orig.nowPlayingScrollViewModelWithDidLoadComponentsFor(
track,
withDifferentProviders: withDifferentProviders,
scrollEnabledValueChanged: scrollEnabledValueChanged

View File

@@ -143,7 +143,7 @@ func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data
color = Color(hex: extractedColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}
else if let uiColor = nowPlayingScrollViewController?.backgroundViewController.color() {
else if let uiColor = nowPlayingScrollViewController?.backgroundViewModel.color() {
color = Color(uiColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}

View File

@@ -41,9 +41,15 @@ extension NowPlayingScrollViewController {
//
var backgroundViewController: SPTNowPlayingBackgroundViewController {
private var backgroundViewController: NSObject {
get {
Ivars<SPTNowPlayingBackgroundViewController>(self).backgroundViewController
Ivars<NSObject>(self).backgroundViewController
}
}
var backgroundViewModel: SPTNowPlayingBackgroundViewModel {
get {
Ivars<SPTNowPlayingBackgroundViewModel>(self.backgroundViewController).viewModel
}
}
}

View File

@@ -1,5 +0,0 @@
import UIKit
@objc protocol SPTNowPlayingBackgroundViewController {
func color() -> UIColor
}

View File

@@ -0,0 +1,5 @@
import UIKit
@objc protocol SPTNowPlayingBackgroundViewModel {
func color() -> UIColor
}

View File

@@ -9,79 +9,103 @@ func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
}
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["payments-initial-campaign"] = AccountAttribute.with {
$0.stringValue = "default"
}
let oneYearFromNow = Calendar.current.date(byAdding: .year, value: 1, to: Date())!
//
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["on-demand"] = AccountAttribute.with {
$0.boolValue = true
}
//
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
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 // allow downloading
}
attributes["nft-disabled"] = AccountAttribute.with {
attributes["audio-quality"] = AccountAttribute.with {
$0.stringValue = "1"
}
attributes["can_use_superbird"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["catalogue"] = AccountAttribute.with {
$0.stringValue = "premium"
}
attributes["financial-product"] = AccountAttribute.with {
$0.stringValue = "pr:premium,tc:0"
}
attributes["high-bitrate"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["is-eligible-premium-unboxing"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["name"] = AccountAttribute.with {
$0.stringValue = "Spotify Premium"
}
attributes["nft-disabled"] = AccountAttribute.with {
$0.stringValue = "1"
}
attributes["offline"] = AccountAttribute.with {
$0.boolValue = true // allow downloading
}
attributes["on-demand"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["payments-initial-campaign"] = AccountAttribute.with {
$0.stringValue = "default"
}
attributes["player-license"] = AccountAttribute.with {
$0.stringValue = "premium"
}
attributes["player-license-v2"] = AccountAttribute.with {
$0.stringValue = "premium"
}
attributes["product-expiry"] = AccountAttribute.with {
$0.stringValue = formatter.string(from: oneYearFromNow)
}
attributes["public-toplist"] = AccountAttribute.with {
$0.stringValue = "1"
}
attributes["shuffle-eligible"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["social-session"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["social-session-free-tier"] = AccountAttribute.with {
$0.boolValue = false
}
//
attributes["com.spotify.madprops.delivered.by.ucs"] = AccountAttribute.with {
$0.boolValue = true
}
attributes["com.spotify.madprops.use.ucs.product.state"] = AccountAttribute.with {
attributes["streaming-rules"] = AccountAttribute.with {
$0.stringValue = ""
}
attributes["subscription-enddate"] = AccountAttribute.with {
$0.stringValue = formatter.string(from: oneYearFromNow)
}
attributes["type"] = AccountAttribute.with {
$0.stringValue = "premium"
}
attributes["unrestricted"] = AccountAttribute.with {
$0.boolValue = true
}
attributes.removeValue(forKey: "payment-state")
attributes.removeValue(forKey: "last-premium-activation-date")
}

View File

@@ -0,0 +1,45 @@
import Foundation
func getPremiumPlanRowData(originalPremiumPlanRow: PremiumPlanRow) throws -> Data {
var premiumPlanRow = originalPremiumPlanRow
premiumPlanRow.planName = "EeveeSpotify"
premiumPlanRow.planIdentifier = "Eevee"
premiumPlanRow.colorCode = "#FFD2D7"
return try premiumPlanRow.serializedData()
}
func getPlanOverviewData() throws -> Data {
let plan = SpotifyPlan.with {
$0.notice = SpotifyPlan.Notice.with {
$0.message = "payment_notice".localized
$0.status = 2 // 0 - trial, 1 - prepaid, 2 - subsription
}
$0.subscription = SpotifyPlan.SubscriptionInfo.with {
$0.planVariant = 2
$0.planName = "EeveeSpotify"
$0.planCategory = "Eevee"
$0.colorCode = "#FFD2D7"
$0.features = [
SpotifyPlan.Feature.with {
$0.color = "#1ED760"
$0.description_p = "ad_free_music_listening".localized
$0.icon = SpotifyPlan.IconType.check
},
SpotifyPlan.Feature.with {
$0.color = "#1ED760"
$0.description_p = "play_songs_in_any_order".localized
$0.icon = SpotifyPlan.IconType.check
},
SpotifyPlan.Feature.with {
$0.color = "#1ED760"
$0.description_p = "organize_listening_queue".localized
$0.icon = SpotifyPlan.IconType.check
}
]
}
}
return try plan.serializedData()
}

View File

@@ -1,7 +1,6 @@
import Foundation
extension BootstrapMessage {
var ucsResponse: UcsResponse {
get {
self.wrapper.oneMoreWrapper.message.response

View File

@@ -1,7 +1,6 @@
import Foundation
extension UcsResponse {
var assignedValues: [AssignedValue] {
get {
self.resolve.configuration.assignedValues

View File

@@ -0,0 +1,132 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: pam.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
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
}
/// Message emitted by pam-view-service
/// GET https://spclient.wg.spotify.com/pam-view-service/v1/GetPremiumPlanRow
struct PremiumPlanRow: Sendable {
// 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 planName: String = String()
var colorCode: String = String()
var planTypeID: Int32 = 0
var billingLabel: String = String()
var actionText: String = String()
var flag: Int32 = 0
var availabilityMessage: String = String()
var durationText: String = String()
var planCategory: Int32 = 0
var planIdentifier: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension PremiumPlanRow: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PremiumPlanRow"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
3: .standard(proto: "plan_name"),
4: .standard(proto: "color_code"),
6: .standard(proto: "plan_type_id"),
8: .standard(proto: "billing_label"),
9: .standard(proto: "action_text"),
10: .same(proto: "flag"),
12: .standard(proto: "availability_message"),
14: .standard(proto: "duration_text"),
16: .standard(proto: "plan_category"),
18: .standard(proto: "plan_identifier"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 3: try { try decoder.decodeSingularStringField(value: &self.planName) }()
case 4: try { try decoder.decodeSingularStringField(value: &self.colorCode) }()
case 6: try { try decoder.decodeSingularInt32Field(value: &self.planTypeID) }()
case 8: try { try decoder.decodeSingularStringField(value: &self.billingLabel) }()
case 9: try { try decoder.decodeSingularStringField(value: &self.actionText) }()
case 10: try { try decoder.decodeSingularInt32Field(value: &self.flag) }()
case 12: try { try decoder.decodeSingularStringField(value: &self.availabilityMessage) }()
case 14: try { try decoder.decodeSingularStringField(value: &self.durationText) }()
case 16: try { try decoder.decodeSingularInt32Field(value: &self.planCategory) }()
case 18: try { try decoder.decodeSingularStringField(value: &self.planIdentifier) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.planName.isEmpty {
try visitor.visitSingularStringField(value: self.planName, fieldNumber: 3)
}
if !self.colorCode.isEmpty {
try visitor.visitSingularStringField(value: self.colorCode, fieldNumber: 4)
}
if self.planTypeID != 0 {
try visitor.visitSingularInt32Field(value: self.planTypeID, fieldNumber: 6)
}
if !self.billingLabel.isEmpty {
try visitor.visitSingularStringField(value: self.billingLabel, fieldNumber: 8)
}
if !self.actionText.isEmpty {
try visitor.visitSingularStringField(value: self.actionText, fieldNumber: 9)
}
if self.flag != 0 {
try visitor.visitSingularInt32Field(value: self.flag, fieldNumber: 10)
}
if !self.availabilityMessage.isEmpty {
try visitor.visitSingularStringField(value: self.availabilityMessage, fieldNumber: 12)
}
if !self.durationText.isEmpty {
try visitor.visitSingularStringField(value: self.durationText, fieldNumber: 14)
}
if self.planCategory != 0 {
try visitor.visitSingularInt32Field(value: self.planCategory, fieldNumber: 16)
}
if !self.planIdentifier.isEmpty {
try visitor.visitSingularStringField(value: self.planIdentifier, fieldNumber: 18)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: PremiumPlanRow, rhs: PremiumPlanRow) -> Bool {
if lhs.planName != rhs.planName {return false}
if lhs.colorCode != rhs.colorCode {return false}
if lhs.planTypeID != rhs.planTypeID {return false}
if lhs.billingLabel != rhs.billingLabel {return false}
if lhs.actionText != rhs.actionText {return false}
if lhs.flag != rhs.flag {return false}
if lhs.availabilityMessage != rhs.availabilityMessage {return false}
if lhs.durationText != rhs.durationText {return false}
if lhs.planCategory != rhs.planCategory {return false}
if lhs.planIdentifier != rhs.planIdentifier {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View File

@@ -0,0 +1,567 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: SpotifyPlan.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
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 SpotifyPlan: Sendable {
// 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 subscription: SpotifyPlan.SubscriptionInfo {
get {return _subscription ?? SpotifyPlan.SubscriptionInfo()}
set {_subscription = newValue}
}
/// Returns true if `subscription` has been explicitly set.
var hasSubscription: Bool {return self._subscription != nil}
/// Clears the value of `subscription`. Subsequent reads from it will return its default value.
mutating func clearSubscription() {self._subscription = nil}
var notice: SpotifyPlan.Notice {
get {return _notice ?? SpotifyPlan.Notice()}
set {_notice = newValue}
}
/// Returns true if `notice` has been explicitly set.
var hasNotice: Bool {return self._notice != nil}
/// Clears the value of `notice`. Subsequent reads from it will return its default value.
mutating func clearNotice() {self._notice = nil}
var actions: [SpotifyPlan.Action] = []
var unknownFields = SwiftProtobuf.UnknownStorage()
enum IconType: SwiftProtobuf.Enum, Swift.CaseIterable {
typealias RawValue = Int
case unspecifiedIcon // = 0
case check // = 1
case chevron // = 2
case ads // = 3
case offline // = 4
case arrow // = 5
case UNRECOGNIZED(Int)
init() {
self = .unspecifiedIcon
}
init?(rawValue: Int) {
switch rawValue {
case 0: self = .unspecifiedIcon
case 1: self = .check
case 2: self = .chevron
case 3: self = .ads
case 4: self = .offline
case 5: self = .arrow
default: self = .UNRECOGNIZED(rawValue)
}
}
var rawValue: Int {
switch self {
case .unspecifiedIcon: return 0
case .check: return 1
case .chevron: return 2
case .ads: return 3
case .offline: return 4
case .arrow: return 5
case .UNRECOGNIZED(let i): return i
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
static let allCases: [SpotifyPlan.IconType] = [
.unspecifiedIcon,
.check,
.chevron,
.ads,
.offline,
.arrow,
]
}
struct SubscriptionInfo: Sendable {
// 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 planType: Int32 = 0
var planVariant: Int32 = 0
var planName: String = String()
var planCategory: String = String()
var colorCode: String = String()
var backgroundImageURL: String = String()
var features: [SpotifyPlan.Feature] = []
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
struct Feature: Sendable {
// 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 icon: SpotifyPlan.IconType = .unspecifiedIcon
var description_p: String = String()
var color: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
struct Notice: Sendable {
// 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: String = String()
var timestamp: Int64 = 0
var status: Int32 = 0
var settings: SpotifyPlan.Settings {
get {return _settings ?? SpotifyPlan.Settings()}
set {_settings = newValue}
}
/// Returns true if `settings` has been explicitly set.
var hasSettings: Bool {return self._settings != nil}
/// Clears the value of `settings`. Subsequent reads from it will return its default value.
mutating func clearSettings() {self._settings = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _settings: SpotifyPlan.Settings? = nil
}
struct Settings: Sendable {
// 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 autoRenew: Int32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
struct Action: Sendable {
// 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 actionID: Int32 = 0
var details: SpotifyPlan.ActionDetails {
get {return _details ?? SpotifyPlan.ActionDetails()}
set {_details = newValue}
}
/// Returns true if `details` has been explicitly set.
var hasDetails: Bool {return self._details != nil}
/// Clears the value of `details`. Subsequent reads from it will return its default value.
mutating func clearDetails() {self._details = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _details: SpotifyPlan.ActionDetails? = nil
}
struct ActionDetails: Sendable {
// 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 title: String = String()
var subtitle: String = String()
var url: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
init() {}
fileprivate var _subscription: SpotifyPlan.SubscriptionInfo? = nil
fileprivate var _notice: SpotifyPlan.Notice? = nil
}
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension SpotifyPlan: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "SpotifyPlan"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "subscription"),
2: .same(proto: "notice"),
3: .same(proto: "actions"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularMessageField(value: &self._subscription) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._notice) }()
case 3: try { try decoder.decodeRepeatedMessageField(value: &self.actions) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._subscription {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
} }()
try { if let v = self._notice {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
if !self.actions.isEmpty {
try visitor.visitRepeatedMessageField(value: self.actions, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan, rhs: SpotifyPlan) -> Bool {
if lhs._subscription != rhs._subscription {return false}
if lhs._notice != rhs._notice {return false}
if lhs.actions != rhs.actions {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.IconType: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "UNSPECIFIED_ICON"),
1: .same(proto: "CHECK"),
2: .same(proto: "CHEVRON"),
3: .same(proto: "ADS"),
4: .same(proto: "OFFLINE"),
5: .same(proto: "ARROW"),
]
}
extension SpotifyPlan.SubscriptionInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".SubscriptionInfo"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "plan_type"),
2: .standard(proto: "plan_variant"),
3: .standard(proto: "plan_name"),
4: .standard(proto: "plan_category"),
5: .standard(proto: "color_code"),
19: .standard(proto: "background_image_url"),
20: .same(proto: "features"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularInt32Field(value: &self.planType) }()
case 2: try { try decoder.decodeSingularInt32Field(value: &self.planVariant) }()
case 3: try { try decoder.decodeSingularStringField(value: &self.planName) }()
case 4: try { try decoder.decodeSingularStringField(value: &self.planCategory) }()
case 5: try { try decoder.decodeSingularStringField(value: &self.colorCode) }()
case 19: try { try decoder.decodeSingularStringField(value: &self.backgroundImageURL) }()
case 20: try { try decoder.decodeRepeatedMessageField(value: &self.features) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.planType != 0 {
try visitor.visitSingularInt32Field(value: self.planType, fieldNumber: 1)
}
if self.planVariant != 0 {
try visitor.visitSingularInt32Field(value: self.planVariant, fieldNumber: 2)
}
if !self.planName.isEmpty {
try visitor.visitSingularStringField(value: self.planName, fieldNumber: 3)
}
if !self.planCategory.isEmpty {
try visitor.visitSingularStringField(value: self.planCategory, fieldNumber: 4)
}
if !self.colorCode.isEmpty {
try visitor.visitSingularStringField(value: self.colorCode, fieldNumber: 5)
}
if !self.backgroundImageURL.isEmpty {
try visitor.visitSingularStringField(value: self.backgroundImageURL, fieldNumber: 19)
}
if !self.features.isEmpty {
try visitor.visitRepeatedMessageField(value: self.features, fieldNumber: 20)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.SubscriptionInfo, rhs: SpotifyPlan.SubscriptionInfo) -> Bool {
if lhs.planType != rhs.planType {return false}
if lhs.planVariant != rhs.planVariant {return false}
if lhs.planName != rhs.planName {return false}
if lhs.planCategory != rhs.planCategory {return false}
if lhs.colorCode != rhs.colorCode {return false}
if lhs.backgroundImageURL != rhs.backgroundImageURL {return false}
if lhs.features != rhs.features {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.Feature: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".Feature"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "icon"),
2: .same(proto: "description"),
4: .same(proto: "color"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularEnumField(value: &self.icon) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.description_p) }()
case 4: try { try decoder.decodeSingularStringField(value: &self.color) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.icon != .unspecifiedIcon {
try visitor.visitSingularEnumField(value: self.icon, fieldNumber: 1)
}
if !self.description_p.isEmpty {
try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 2)
}
if !self.color.isEmpty {
try visitor.visitSingularStringField(value: self.color, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.Feature, rhs: SpotifyPlan.Feature) -> Bool {
if lhs.icon != rhs.icon {return false}
if lhs.description_p != rhs.description_p {return false}
if lhs.color != rhs.color {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.Notice: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".Notice"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "message"),
4: .same(proto: "timestamp"),
7: .same(proto: "status"),
8: .same(proto: "settings"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.message) }()
case 4: try { try decoder.decodeSingularInt64Field(value: &self.timestamp) }()
case 7: try { try decoder.decodeSingularInt32Field(value: &self.status) }()
case 8: try { try decoder.decodeSingularMessageField(value: &self._settings) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.message.isEmpty {
try visitor.visitSingularStringField(value: self.message, fieldNumber: 1)
}
if self.timestamp != 0 {
try visitor.visitSingularInt64Field(value: self.timestamp, fieldNumber: 4)
}
if self.status != 0 {
try visitor.visitSingularInt32Field(value: self.status, fieldNumber: 7)
}
try { if let v = self._settings {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
} }()
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.Notice, rhs: SpotifyPlan.Notice) -> Bool {
if lhs.message != rhs.message {return false}
if lhs.timestamp != rhs.timestamp {return false}
if lhs.status != rhs.status {return false}
if lhs._settings != rhs._settings {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.Settings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".Settings"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
2: .standard(proto: "auto_renew"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 2: try { try decoder.decodeSingularInt32Field(value: &self.autoRenew) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.autoRenew != 0 {
try visitor.visitSingularInt32Field(value: self.autoRenew, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.Settings, rhs: SpotifyPlan.Settings) -> Bool {
if lhs.autoRenew != rhs.autoRenew {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.Action: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".Action"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "action_id"),
2: .same(proto: "details"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularInt32Field(value: &self.actionID) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._details) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if self.actionID != 0 {
try visitor.visitSingularInt32Field(value: self.actionID, fieldNumber: 1)
}
try { if let v = self._details {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.Action, rhs: SpotifyPlan.Action) -> Bool {
if lhs.actionID != rhs.actionID {return false}
if lhs._details != rhs._details {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SpotifyPlan.ActionDetails: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SpotifyPlan.protoMessageName + ".ActionDetails"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "title"),
2: .same(proto: "subtitle"),
3: .same(proto: "url"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.title) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.subtitle) }()
case 3: try { try decoder.decodeSingularStringField(value: &self.url) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.title.isEmpty {
try visitor.visitSingularStringField(value: self.title, fieldNumber: 1)
}
if !self.subtitle.isEmpty {
try visitor.visitSingularStringField(value: self.subtitle, fieldNumber: 2)
}
if !self.url.isEmpty {
try visitor.visitSingularStringField(value: self.url, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SpotifyPlan.ActionDetails, rhs: SpotifyPlan.ActionDetails) -> Bool {
if lhs.title != rhs.title {return false}
if lhs.subtitle != rhs.subtitle {return false}
if lhs.url != rhs.url {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View File

@@ -4,6 +4,14 @@ extension URL {
var isLyrics: Bool {
self.path.contains("color-lyrics/v2")
}
var isPlanOverview: Bool {
self.path.contains("GetPlanOverview")
}
var isPremiumPlanRow: Bool {
self.path.contains("v1/GetPremiumPlanRow")
}
var isOpenSpotifySafariExtension: Bool {
self.host == "eevee"

View File

@@ -11,7 +11,7 @@ func exitApplication() {
struct PremiumPatchingGroup: HookGroup { }
struct EeveeSpotify: Tweak {
static let version = "6.0.2"
static let version = "6.1"
static var hookTarget: VersionHookTarget {
let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String

View File

@@ -1,6 +1,6 @@
Package: com.eevee.spotify
Name: EeveeSpotify
Version: 6.0.2
Version: 6.1
Architecture: iphoneos-arm
Description: A tweak to get Spotify Premium for free, just like Spotilife
Maintainer: Eevee

View File

@@ -131,3 +131,11 @@ request_anonymous_token = "Request Anonymous Token";
request_anonymous_token_description = "Tap “Request Anonymous Token” to request a token from Musixmatch without authorization.";
lrclib_api = "Server Address";
// Snapshot of your benefits, should match official spotify loc
ad_free_music_listening = "Ad-free music listening";
play_songs_in_any_order = "Play songs in any order";
organize_listening_queue = "Organize listening queue";
payment_notice = "EeveeSpotify applies patches to unlock certain Premium features. It's free.";

View File

@@ -129,3 +129,11 @@ request_anonymous_token = "Запросить анонимный токен";
request_anonymous_token_description = "Нажмите Запросить анонимный токен, чтобы запросить токен у Musixmatch без авторизации.";
lrclib_api = "Адрес сервера";
// Snapshot of your benefits, should match official spotify loc
ad_free_music_listening = "Музыка без рекламы";
play_songs_in_any_order = "Треки в любом порядке";
organize_listening_queue = "Добавление треков в очередь";
payment_notice = "EeveeSpotify применяет патчи, которые активируют некоторые возможности Premium. Это бесплатно.";