something

This commit is contained in:
eevee
2024-06-02 14:54:25 +03:00
parent 400a28924a
commit e258ebc35b
23 changed files with 461 additions and 381 deletions

View File

@@ -1 +1,7 @@
{ Filter = { Bundles = ( "com.spotify.client" ); }; }
{
Filter = {
Bundles = (
"com.spotify.client",
);
};
}

View File

@@ -114,7 +114,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
OfflineHelper.dataBuffer = Data()
var customizeMessage = try CustomizeMessage(serializedData: buffer)
modifyAttributes(&customizeMessage.response.attributes.accountAttributes)
modifyRemoteConfiguration(&customizeMessage.response)
orig.URLSession(
session,

View File

@@ -1,6 +1,5 @@
import UIKit
import Orion
import Foundation
class PopUpHelper {

View File

@@ -1,5 +1,4 @@
import UIKit
import Foundation
class WindowHelper {

View File

@@ -1,5 +1,4 @@
import UIKit
import Foundation
class LyricsHelper {
@@ -40,7 +39,7 @@ class LyricsHelper {
lyricLines = lines.map { line in
let match = line.firstMatch(
"\\[(?<minute>\\d{2}):(?<seconds>\\d{2}\\.\\d{2})\\] ?(?<content>.*)"
"\\[(?<minute>\\d{2}):(?<seconds>\\d{2}\\.?\\d*)\\] ?(?<content>.*)"
)!
var captures: [String: String] = [:]

View File

@@ -23,4 +23,4 @@ enum GeniusDataResponse: Decodable {
)
}
}
}
}

View File

@@ -3,4 +3,4 @@ import Foundation
struct GeniusHitResult: Decodable {
var id: Int
var title: String
}
}

View File

@@ -44,4 +44,4 @@ extension String {
withTemplate: ""
)
}
}
}

View File

@@ -0,0 +1,84 @@
import Foundation
func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
modifyAttributes(&configuration.attributes.accountAttributes)
}
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"
}
//
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
}
//
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["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 {
$0.boolValue = true
}
}

View File

@@ -20,70 +20,6 @@ func showOfflineBnkMethodSetPopUp() {
)
}
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"
@@ -128,7 +64,7 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
if UserDefaults.patchType == .requests {
modifyAttributes(&bootstrapMessage.attributes)
modifyRemoteConfiguration(&bootstrapMessage.ucsResponse)
orig.URLSession(
session,

View File

@@ -1,20 +1,22 @@
//
// File.swift
//
//
// Created by eevee on 28/05/2024.
//
import Foundation
extension BootstrapMessage {
var attributes: [String: AccountAttribute] {
var ucsResponse: UcsResponse {
get {
self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes
self.wrapper.oneMoreWrapper.message.response
}
set(ucsResponse) {
self.wrapper.oneMoreWrapper.message.response = ucsResponse
}
}
var attributes: Dictionary<String, AccountAttribute> {
get {
self.ucsResponse.attributes.accountAttributes
}
set(attributes) {
self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes = attributes
self.ucsResponse.attributes.accountAttributes = attributes
}
}
}

View File

@@ -0,0 +1,13 @@
import Foundation
extension UcsResponse {
var assignedValues: [AssignedValue] {
get {
self.resolve.configuration.assignedValues
}
set(assignedValues) {
self.resolve.configuration.assignedValues = assignedValues
}
}
}

View File

@@ -0,0 +1,25 @@
import Orion
class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook<NSObject> {
static let targetName = "SPTFreeTierArtistHubRemoteURLResolver"
func initWithViewURI(
_ uri: NSURL,
onDemandSet: Any,
onDemandTrialService: Any,
trackRowsEnabled: Bool,
productState: SPTCoreProductState
) -> Target {
return orig.initWithViewURI(
uri,
onDemandSet: onDemandSet,
onDemandTrialService: onDemandTrialService,
trackRowsEnabled: UserDefaults.patchType.isPatching
? true
: trackRowsEnabled,
productState: productState
)
}
}

View File

@@ -0,0 +1,55 @@
import Orion
import UIKit
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(rootSettingsController.view.bounds)
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)
}
}

View File

@@ -0,0 +1,29 @@
import SwiftUI
import UIKit
class EeveeSettingsViewController: SPTPageViewController {
let frame: CGRect
init(_ frame: CGRect) {
self.frame = frame
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "EeveeSpotify"
let hostingController = UIHostingController(rootView: EeveeSettingsView())
hostingController.view.frame = frame
view.addSubview(hostingController.view)
addChild(hostingController)
hostingController.didMove(toParent: self)
}
}

View File

@@ -0,0 +1,21 @@
import UIKit
class SPTPageViewController: UIViewController {
override func conforms(to aProtocol: Protocol) -> Bool {
if NSStringFromProtocol(aProtocol) ~= "SPTPageController" {
return true
}
return super.conforms(to: aProtocol)
}
@objc func spt_pageIdentifier() -> String? {
return "EeveeSpotify"
}
@objc func spt_pageURI() -> NSURL? {
return NSURL(string: "")
}
}

View File

@@ -0,0 +1,35 @@
import SwiftUI
extension EeveeSettingsView {
@ViewBuilder func PremiumSection() -> some View {
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)
}
}
}
}
}

View File

@@ -0,0 +1,118 @@
import SwiftUI
import UIKit
struct EeveeSettingsView: View {
@State var musixmatchToken = UserDefaults.musixmatchToken
@State var patchType = UserDefaults.patchType
@State var lyricsSource = UserDefaults.lyricsSource
private func showMusixmatchTokenAlert(_ oldSource: LyricsSource) {
let alert = UIAlertController(
title: "Enter User Token",
message: "In order to use Musixmatch, you need to retrieve your user token from the official app. Download Musixmatch from the App Store, sign up, then go to Settings > Get help > Copy debug info, and paste it here. You can also extract the token using MITM.",
preferredStyle: .alert
)
alert.addTextField() { textField in
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
lyricsSource = oldSource
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
let text = alert.textFields!.first!.text!
let token: String
if let match = text.firstMatch("\\[UserToken\\]: ([a-f0-9]+)"),
let tokenRange = Range(match.range(at: 1), in: text) {
token = String(text[tokenRange])
}
else if text ~= "^[a-f0-9]+$" {
token = text
}
else {
lyricsSource = oldSource
return
}
musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
var body: some View {
List {
PremiumSection()
LyricsSections()
Section {
Toggle(
"Dark PopUps",
isOn: Binding<Bool>(
get: { UserDefaults.darkPopUps },
set: { UserDefaults.darkPopUps = $0 }
)
)
}
Section(footer: Text("Clear cached data and restart the app.")) {
Button {
try! OfflineHelper.resetPersistentCache()
exitApplication()
} label: {
Text("Reset Data")
}
}
}
.padding(.bottom, 45)
.animation(.default, value: lyricsSource)
.animation(.default, value: patchType)
.onChange(of: musixmatchToken) { token in
UserDefaults.musixmatchToken = token
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource)
return
}
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())
.onAppear {
UIView.appearance(
whenContainedInInstancesOf: [UIAlertController.self]
).tintColor = UIColor(Color(hex: "#1ed760"))
WindowHelper.shared.overrideUserInterfaceStyle(.dark)
}
}
}

View File

@@ -0,0 +1,55 @@
import SwiftUI
extension EeveeSettingsView {
@ViewBuilder func LyricsSections() -> some View {
Section(footer: Text("""
You can select the lyrics source you prefer.
Genius: Offers the best quality lyrics, provides the most songs, and updates lyrics the fastest. Does not and will never be time-synced.
LRCLIB: The most open service, offering time-synced lyrics. However, it lacks lyrics for many songs.
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 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",
selection: $lyricsSource
) {
Text("Genius").tag(LyricsSource.genius)
Text("LRCLIB").tag(LyricsSource.lrclib)
Text("Musixmatch").tag(LyricsSource.musixmatch)
}
if lyricsSource == .musixmatch {
VStack(alignment: .leading, spacing: 5) {
Text("Musixmatch User Token")
TextField("Enter User Token", text: $musixmatchToken)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
if lyricsSource != .genius {
Section(
footer: Text("Load lyrics from Genius if there is a problem with \(lyricsSource).")
) {
Toggle(
"Genius Fallback",
isOn: Binding<Bool>(
get: { UserDefaults.geniusFallback },
set: { UserDefaults.geniusFallback = $0 }
)
)
}
}
}
}

View File

@@ -9,81 +9,6 @@ func exitApplication() {
}
}
class URLHook: ClassHook<NSURL> {
func initWithString(_ urlString: String, relativeToURL URL: NSURL) -> NSURL {
var finalString = urlString
if finalString.contains("artistview") {
finalString = finalString.replacingOccurrences(
of: "trackRows=false",
with: "trackRows=true"
)
finalString = finalString.replacingOccurrences(
of: "video=false",
with: "video=true"
)
}
return orig.initWithString(finalString, relativeToURL: URL)
}
}
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 = "4.0"

View File

@@ -1,188 +0,0 @@
import SwiftUI
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) {
let alert = UIAlertController(
title: "Enter User Token",
message: "In order to use Musixmatch, you need to retrieve your user token from the official app. Download Musixmatch from the App Store, sign up, then go to Settings > Get help > Copy debug info, and paste it here. You can also extract the token using MITM.",
preferredStyle: .alert
)
alert.addTextField() { textField in
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
lyricsSource = oldSource
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
let text = alert.textFields!.first!.text!
let token: String
if let match = text.firstMatch("\\[UserToken\\]: ([a-f0-9]+)"),
let tokenRange = Range(match.range(at: 1), in: text) {
token = String(text[tokenRange])
}
else if text ~= "^[a-f0-9]+$" {
token = text
}
else {
lyricsSource = oldSource
return
}
musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
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.
Genius: Offers the best quality lyrics, provides the most songs, and updates lyrics the fastest. Does not and will never be time-synced.
LRCLIB: The most open service, offering time-synced lyrics. However, it lacks lyrics for many songs.
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 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",
selection: $lyricsSource
) {
Text("Genius").tag(LyricsSource.genius)
Text("LRCLIB").tag(LyricsSource.lrclib)
Text("Musixmatch").tag(LyricsSource.musixmatch)
}
if lyricsSource == .musixmatch {
VStack(alignment: .leading, spacing: 5) {
Text("Musixmatch User Token")
TextField("Enter User Token", text: $musixmatchToken)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
if lyricsSource != .genius {
Section(
footer: Text("Load lyrics from Genius if there is a problem with \(lyricsSource).")
) {
Toggle(
"Genius Fallback",
isOn: Binding<Bool>(
get: { UserDefaults.geniusFallback },
set: { UserDefaults.geniusFallback = $0 }
)
)
}
}
Section {
Toggle(
"Dark PopUps",
isOn: Binding<Bool>(
get: { UserDefaults.darkPopUps },
set: { UserDefaults.darkPopUps = $0 }
)
)
}
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
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource)
return
}
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())
.onAppear {
UIView.appearance(
whenContainedInInstancesOf: [UIAlertController.self]
).tintColor = UIColor(Color(hex: "#1ed760"))
WindowHelper.shared.overrideUserInterfaceStyle(.dark)
}
}
}

View File

@@ -1,33 +0,0 @@
import SwiftUI
import UIKit
class EeveeSettingsViewController: UIViewController {
override func conforms(to aProtocol: Protocol) -> Bool {
if NSStringFromProtocol(aProtocol) ~= "SPTPageController" {
return true
}
return super.conforms(to: aProtocol)
}
@objc func spt_pageIdentifier() -> String? {
return "EeveeSpotify"
}
@objc func spt_pageURI() -> NSURL? {
return NSURL(string: "")
}
override func viewDidLoad() {
super.viewDidLoad()
let hostingController = UIHostingController(rootView: EeveeSettingsView())
hostingController.view.frame = view.bounds
view.addSubview(hostingController.view)
addChild(hostingController)
hostingController.didMove(toParent: self)
}
}

View File

@@ -4,7 +4,7 @@ Before opening an issue, please note that **EeveeSpotify does not accept feature
## All tracks are skipped
EeveeSpotify doesn't work in some regions, try connecting to a VPN server in the United States or other region, then change your country on [Spotify's website](https://accounts.spotify.com). After changing your country, you should sign out and sign back in to Spotify with your VPN on.
Connect to a VPN server in any region, then change your country on [Spotify's website](https://accounts.spotify.com). After changing your country, you should sign out and sign back in to Spotify with your VPN on.
References: https://github.com/whoeevee/EeveeSpotify/issues/67, https://github.com/whoeevee/EeveeSpotify/issues/152