v4.0: Dynamic Premium Patching Method, Disable Patching and Reset Data Options

This commit is contained in:
eevee
2024-05-28 18:33:11 +03:00
parent fb8a974429
commit 171af9cc9b
18 changed files with 1985 additions and 272 deletions
+1
View File
@@ -6,6 +6,7 @@ xcuserdata/
._*
.theos
.swiftpm
/packages
.theos/
packages/
-215
View File
@@ -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)
}
}
@@ -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
}
}
@@ -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()
}
)
@@ -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)
}
}
}
+116 -32
View File
@@ -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)
}
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
Package: com.eevee.spotify
Name: EeveeSpotify
Version: 3.1
Version: 4.0
Architecture: iphoneos-arm
Description: A tweak to get Spotify Premium for free, just like Spotilife
Maintainer: Eevee