mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-08 23:23:20 +00:00
something
This commit is contained in:
@@ -1 +1,7 @@
|
||||
{ Filter = { Bundles = ( "com.spotify.client" ); }; }
|
||||
{
|
||||
Filter = {
|
||||
Bundles = (
|
||||
"com.spotify.client",
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import UIKit
|
||||
import Orion
|
||||
import Foundation
|
||||
|
||||
class PopUpHelper {
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
class WindowHelper {
|
||||
|
||||
|
||||
@@ -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] = [:]
|
||||
|
||||
@@ -23,4 +23,4 @@ enum GeniusDataResponse: Decodable {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ import Foundation
|
||||
struct GeniusHitResult: Decodable {
|
||||
var id: Int
|
||||
var title: String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@ extension String {
|
||||
withTemplate: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
extension UcsResponse {
|
||||
|
||||
var assignedValues: [AssignedValue] {
|
||||
get {
|
||||
self.resolve.configuration.assignedValues
|
||||
}
|
||||
set(assignedValues) {
|
||||
self.resolve.configuration.assignedValues = assignedValues
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift
Normal file
25
Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
55
Sources/EeveeSpotify/Settings/EeveeSettings.x.swift
Normal file
55
Sources/EeveeSpotify/Settings/EeveeSettings.x.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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: "")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift
Normal file
118
Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user