From e258ebc35bb999d97cc301a711309efe66493a75 Mon Sep 17 00:00:00 2001 From: eevee Date: Sun, 2 Jun 2024 14:54:25 +0300 Subject: [PATCH] something --- EeveeSpotify.plist | 8 +- .../DataLoaderServiceHooks.x.swift | 2 +- .../EeveeSpotify/Helpers/PopUpHelper.swift | 1 - .../EeveeSpotify/Helpers/WindowHelper.swift | 1 - .../Lyrics/Helpers/LyricsHelper.swift | 3 +- .../Models/Genius/GeniusDataResponse.swift | 2 +- .../Models/Genius/GeniusHitResult.swift | 2 +- .../Models/Extensions/String+Extension.swift | 2 +- .../DynamicPremium+ModifyFunctions.swift | 84 ++++++++ .../Premium/DynamicPremium.x.swift | 66 +----- .../BootstrapMessage+Extension.swift | 22 +- .../Extensions/UcsResponse+Extension.swift | 13 ++ .../Premium/TrackRowsEnabler.x.swift | 25 +++ .../Settings/EeveeSettings.x.swift | 55 +++++ .../EeveeSettingsViewController.swift | 29 +++ .../SPTPageViewController.swift | 21 ++ .../EeveeSettingsView+PremiumSection.swift | 35 ++++ .../Settings/Views/EeveeSettingsView.swift | 118 +++++++++++ ...ettingsViewController+LyricsSections.swift | 55 +++++ Sources/EeveeSpotify/Tweak.x.swift | 75 ------- .../Views/EeveeSettingsView.swift | 188 ------------------ .../Views/EeveeSettingsViewController.swift | 33 --- common_issues.md | 2 +- 23 files changed, 461 insertions(+), 381 deletions(-) create mode 100644 Sources/EeveeSpotify/Premium/DynamicPremium+ModifyFunctions.swift create mode 100644 Sources/EeveeSpotify/Premium/Models/Extensions/UcsResponse+Extension.swift create mode 100644 Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift create mode 100644 Sources/EeveeSpotify/Settings/EeveeSettings.x.swift create mode 100644 Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift create mode 100644 Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift create mode 100644 Sources/EeveeSpotify/Settings/Views/EeveeSettingsView+PremiumSection.swift create mode 100644 Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift create mode 100644 Sources/EeveeSpotify/Settings/Views/EeveeSettingsViewController+LyricsSections.swift delete mode 100644 Sources/EeveeSpotify/Views/EeveeSettingsView.swift delete mode 100644 Sources/EeveeSpotify/Views/EeveeSettingsViewController.swift diff --git a/EeveeSpotify.plist b/EeveeSpotify.plist index 4bb2b96..cf2578b 100644 --- a/EeveeSpotify.plist +++ b/EeveeSpotify.plist @@ -1 +1,7 @@ -{ Filter = { Bundles = ( "com.spotify.client" ); }; } +{ + Filter = { + Bundles = ( + "com.spotify.client", + ); + }; +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift index f848f79..5dc1dc1 100644 --- a/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift +++ b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift @@ -114,7 +114,7 @@ class SPTDataLoaderServiceHook: ClassHook { OfflineHelper.dataBuffer = Data() var customizeMessage = try CustomizeMessage(serializedData: buffer) - modifyAttributes(&customizeMessage.response.attributes.accountAttributes) + modifyRemoteConfiguration(&customizeMessage.response) orig.URLSession( session, diff --git a/Sources/EeveeSpotify/Helpers/PopUpHelper.swift b/Sources/EeveeSpotify/Helpers/PopUpHelper.swift index 10b2e8d..a343b9f 100644 --- a/Sources/EeveeSpotify/Helpers/PopUpHelper.swift +++ b/Sources/EeveeSpotify/Helpers/PopUpHelper.swift @@ -1,6 +1,5 @@ import UIKit import Orion -import Foundation class PopUpHelper { diff --git a/Sources/EeveeSpotify/Helpers/WindowHelper.swift b/Sources/EeveeSpotify/Helpers/WindowHelper.swift index a7ebbad..3f77a75 100644 --- a/Sources/EeveeSpotify/Helpers/WindowHelper.swift +++ b/Sources/EeveeSpotify/Helpers/WindowHelper.swift @@ -1,5 +1,4 @@ import UIKit -import Foundation class WindowHelper { diff --git a/Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift b/Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift index f4f4956..015556b 100644 --- a/Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift +++ b/Sources/EeveeSpotify/Lyrics/Helpers/LyricsHelper.swift @@ -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( - "\\[(?\\d{2}):(?\\d{2}\\.\\d{2})\\] ?(?.*)" + "\\[(?\\d{2}):(?\\d{2}\\.?\\d*)\\] ?(?.*)" )! var captures: [String: String] = [:] diff --git a/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusDataResponse.swift b/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusDataResponse.swift index 6dbea64..fc541de 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusDataResponse.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusDataResponse.swift @@ -23,4 +23,4 @@ enum GeniusDataResponse: Decodable { ) } } -} \ No newline at end of file +} diff --git a/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusHitResult.swift b/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusHitResult.swift index bcf832a..763ab3e 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusHitResult.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Genius/GeniusHitResult.swift @@ -3,4 +3,4 @@ import Foundation struct GeniusHitResult: Decodable { var id: Int var title: String -} \ No newline at end of file +} diff --git a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift index bf005ea..55e47f3 100644 --- a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift @@ -44,4 +44,4 @@ extension String { withTemplate: "" ) } -} \ No newline at end of file +} diff --git a/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyFunctions.swift b/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyFunctions.swift new file mode 100644 index 0000000..785bf0d --- /dev/null +++ b/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyFunctions.swift @@ -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 + } +} diff --git a/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift b/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift index e73db92..4314381 100644 --- a/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift +++ b/Sources/EeveeSpotify/Premium/DynamicPremium.x.swift @@ -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 { static let targetName = "SPTCoreURLSessionDataDelegate" @@ -128,7 +64,7 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook { if UserDefaults.patchType == .requests { - modifyAttributes(&bootstrapMessage.attributes) + modifyRemoteConfiguration(&bootstrapMessage.ucsResponse) orig.URLSession( session, diff --git a/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift b/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift index e7de3d7..7403209 100644 --- a/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift +++ b/Sources/EeveeSpotify/Premium/Models/Extensions/BootstrapMessage+Extension.swift @@ -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 + } + set(ucsResponse) { + self.wrapper.oneMoreWrapper.message.response = ucsResponse + } + } + + var attributes: Dictionary { get { - self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes + self.ucsResponse.attributes.accountAttributes } set(attributes) { - self.wrapper.oneMoreWrapper.message.response.attributes.accountAttributes = attributes + self.ucsResponse.attributes.accountAttributes = attributes } } } diff --git a/Sources/EeveeSpotify/Premium/Models/Extensions/UcsResponse+Extension.swift b/Sources/EeveeSpotify/Premium/Models/Extensions/UcsResponse+Extension.swift new file mode 100644 index 0000000..37cbebc --- /dev/null +++ b/Sources/EeveeSpotify/Premium/Models/Extensions/UcsResponse+Extension.swift @@ -0,0 +1,13 @@ +import Foundation + +extension UcsResponse { + + var assignedValues: [AssignedValue] { + get { + self.resolve.configuration.assignedValues + } + set(assignedValues) { + self.resolve.configuration.assignedValues = assignedValues + } + } +} diff --git a/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift b/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift new file mode 100644 index 0000000..7d81ca8 --- /dev/null +++ b/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift @@ -0,0 +1,25 @@ +import Orion + +class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook { + + 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 + ) + } +} diff --git a/Sources/EeveeSpotify/Settings/EeveeSettings.x.swift b/Sources/EeveeSpotify/Settings/EeveeSettings.x.swift new file mode 100644 index 0000000..3af659c --- /dev/null +++ b/Sources/EeveeSpotify/Settings/EeveeSettings.x.swift @@ -0,0 +1,55 @@ +import Orion +import UIKit + +class ProfileSettingsSectionHook: ClassHook { + + static let targetName = "ProfileSettingsSection" + + func numberOfRows() -> Int { + return 2 + } + + func didSelectRow(_ row: Int) { + + if row == 1 { + + let rootSettingsController = WindowHelper.shared.findFirstViewController( + "RootSettingsViewController" + )! + + let eeveeSettingsController = EeveeSettingsViewController(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) + } +} diff --git a/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift b/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift new file mode 100644 index 0000000..ffc7dd5 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift @@ -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) + } +} diff --git a/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift b/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift new file mode 100644 index 0000000..39abc34 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift @@ -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: "") + } +} diff --git a/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView+PremiumSection.swift b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView+PremiumSection.swift new file mode 100644 index 0000000..ffdd3a1 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView+PremiumSection.swift @@ -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( + 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) + } + } + } + } +} diff --git a/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift new file mode 100644 index 0000000..b74d71a --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsView.swift @@ -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( + 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) + } + } +} diff --git a/Sources/EeveeSpotify/Settings/Views/EeveeSettingsViewController+LyricsSections.swift b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsViewController+LyricsSections.swift new file mode 100644 index 0000000..4678b20 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Views/EeveeSettingsViewController+LyricsSections.swift @@ -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( + get: { UserDefaults.geniusFallback }, + set: { UserDefaults.geniusFallback = $0 } + ) + ) + } + } + } +} + diff --git a/Sources/EeveeSpotify/Tweak.x.swift b/Sources/EeveeSpotify/Tweak.x.swift index 6b7be71..10be517 100644 --- a/Sources/EeveeSpotify/Tweak.x.swift +++ b/Sources/EeveeSpotify/Tweak.x.swift @@ -9,81 +9,6 @@ func exitApplication() { } } -class URLHook: ClassHook { - - 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 { - - 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" diff --git a/Sources/EeveeSpotify/Views/EeveeSettingsView.swift b/Sources/EeveeSpotify/Views/EeveeSettingsView.swift deleted file mode 100644 index 6b6595e..0000000 --- a/Sources/EeveeSpotify/Views/EeveeSettingsView.swift +++ /dev/null @@ -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( - 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( - get: { UserDefaults.geniusFallback }, - set: { UserDefaults.geniusFallback = $0 } - ) - ) - } - } - - Section { - Toggle( - "Dark PopUps", - isOn: Binding( - 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) - } - } -} diff --git a/Sources/EeveeSpotify/Views/EeveeSettingsViewController.swift b/Sources/EeveeSpotify/Views/EeveeSettingsViewController.swift deleted file mode 100644 index c16dac8..0000000 --- a/Sources/EeveeSpotify/Views/EeveeSettingsViewController.swift +++ /dev/null @@ -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) - } -} diff --git a/common_issues.md b/common_issues.md index 3903725..e784e26 100644 --- a/common_issues.md +++ b/common_issues.md @@ -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