mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-09 00:23:20 +01:00
v4.0: Dynamic Premium Patching Method, Disable Patching and Reset Data Options
This commit is contained in:
@@ -6,6 +6,7 @@ xcuserdata/
|
||||
|
||||
._*
|
||||
.theos
|
||||
.swiftpm
|
||||
/packages
|
||||
.theos/
|
||||
packages/
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import Orion
|
||||
import SwiftUI
|
||||
|
||||
class SPTPlayerTrackHook: ClassHook<NSObject> {
|
||||
|
||||
static let targetName = "SPTPlayerTrack"
|
||||
|
||||
func setMetadata(_ metadata: [String:String]) {
|
||||
var meta = metadata
|
||||
|
||||
meta["has_lyrics"] = "true"
|
||||
orig.setMetadata(meta)
|
||||
}
|
||||
}
|
||||
|
||||
class EncoreButtonHook: ClassHook<UIButton> {
|
||||
|
||||
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<NSObject> {
|
||||
|
||||
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<NSObject> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import Foundation
|
||||
import Orion
|
||||
|
||||
class SPTDataLoaderServiceHook: ClassHook<NSObject> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import Orion
|
||||
import SwiftUI
|
||||
|
||||
class SPTPlayerTrackHook: ClassHook<NSObject> {
|
||||
|
||||
static let targetName = "SPTPlayerTrack"
|
||||
|
||||
func setMetadata(_ metadata: [String:String]) {
|
||||
var meta = metadata
|
||||
|
||||
meta["has_lyrics"] = "true"
|
||||
orig.setMetadata(meta)
|
||||
}
|
||||
}
|
||||
|
||||
class EncoreButtonHook: ClassHook<UIButton> {
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -8,4 +8,12 @@ extension URL {
|
||||
var isOpenSpotifySafariExtension: Bool {
|
||||
self.host == "eevee"
|
||||
}
|
||||
}
|
||||
|
||||
var isCustomize: Bool {
|
||||
self.path.contains("v1/customize")
|
||||
}
|
||||
|
||||
var isBootstrap: Bool {
|
||||
self.path.contains("v1/bootstrap")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NSObject> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+25
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
enum PatchType: Int {
|
||||
case notSet
|
||||
case disabled
|
||||
case offlineBnk
|
||||
case requests
|
||||
|
||||
var isPatching: Bool {
|
||||
self == .requests || self == .offlineBnk
|
||||
}
|
||||
}
|
||||
+6
-14
@@ -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()
|
||||
}
|
||||
)
|
||||
+8
-2
@@ -1,8 +1,11 @@
|
||||
import Orion
|
||||
import UIKit
|
||||
|
||||
struct ServerSidedReminder: HookGroup { }
|
||||
|
||||
class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
static let targetName = "StreamQualitySettingsSection"
|
||||
|
||||
func shouldResetSelection() -> Bool {
|
||||
@@ -26,7 +29,8 @@ func showOfflineModePopUp() {
|
||||
}
|
||||
|
||||
class FTPDownloadActionHook: ClassHook<NSObject> {
|
||||
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
static let targetName = "ListUXPlatform_FreeTierPlaylistImpl.FTPDownloadAction"
|
||||
|
||||
func execute(_ idk: Any) {
|
||||
@@ -36,6 +40,8 @@ class FTPDownloadActionHook: ClassHook<NSObject> {
|
||||
|
||||
class UIButtonHook: ClassHook<UIButton> {
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
|
||||
func setHighlighted(_ highlighted: Bool) {
|
||||
|
||||
if highlighted {
|
||||
@@ -56,4 +62,4 @@ class UIButtonHook: ClassHook<UIButton> {
|
||||
|
||||
orig.setHighlighted(highlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<NSURL> {
|
||||
|
||||
@@ -21,49 +30,124 @@ class URLHook: ClassHook<NSURL> {
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileSettingsSectionHook: ClassHook<NSObject> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
struct EeveeSpotify: Tweak {
|
||||
|
||||
static let version = "3.1"
|
||||
static let version = "4.0"
|
||||
|
||||
init() {
|
||||
|
||||
do {
|
||||
|
||||
if UserDefaults.darkPopUps {
|
||||
DarkPopUps().activate()
|
||||
}
|
||||
|
||||
defer {
|
||||
NSFileCoordinator.addFilePresenter(OfflineObserver())
|
||||
|
||||
if UserDefaults.darkPopUps {
|
||||
DarkPopUps().activate()
|
||||
}
|
||||
|
||||
let patchType = UserDefaults.patchType
|
||||
|
||||
if patchType.isPatching {
|
||||
|
||||
if patchType == .offlineBnk {
|
||||
NSFileCoordinator.addFilePresenter(OfflineObserver())
|
||||
}
|
||||
|
||||
ServerSidedReminder().activate()
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try OfflineHelper.restoreFromEeveeBnk()
|
||||
NSLog("[EeveeSpotify] Restored from eevee.bnk")
|
||||
|
||||
switch UserDefaults.patchType {
|
||||
|
||||
case .disabled:
|
||||
|
||||
NSLog("[EeveeSpotify] Not activating: patchType is disabled")
|
||||
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: "Okay!"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Bool>(
|
||||
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<Bool>(
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user