This commit is contained in:
eevee
2024-08-09 13:46:38 +03:00
parent 6bc5801315
commit 5ba2b4a659
26 changed files with 324 additions and 405 deletions

View File

@@ -1,8 +1,7 @@
import Foundation
import Orion
class SPTDataLoaderServiceHook: ClassHook<NSObject> {
class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
static let targetName = "SPTDataLoaderService"
// orion:new
@@ -23,48 +22,46 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
return
}
if error == nil && shouldModify(url) {
if let buffer = URLSessionHelper.shared.obtainData(for: url) {
if url.isLyrics {
do {
orig.URLSession(
session,
dataTask: task,
didReceiveData: try getLyricsForCurrentTrack(
originalLyrics: try? Lyrics(serializedData: buffer)
)
)
orig.URLSession(session, task: task, didCompleteWithError: nil)
}
catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
return
}
if error == nil,
shouldModify(url),
let buffer = URLSessionHelper.shared.obtainData(for: url)
{
if url.isLyrics {
do {
var customizeMessage = try CustomizeMessage(serializedData: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
orig.URLSession(
session,
dataTask: task,
didReceiveData: try customizeMessage.serializedData()
didReceiveData: try getLyricsForCurrentTrack(
originalLyrics: try? Lyrics(serializedData: buffer)
)
)
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, task: task, didCompleteWithError: error)
}
return
}
do {
var customizeMessage = try CustomizeMessage(serializedData: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
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)")
}
}
@@ -76,12 +73,16 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
_ session: URLSession,
dataTask task: URLSessionDataTask,
didReceiveResponse response: HTTPURLResponse,
completionHandler handler: Any
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
) {
let url = response.url!
guard
let request = task.currentRequest,
let url = request.url
else {
return
}
if url.isLyrics, response.statusCode != 200 {
let okResponse = HTTPURLResponse(
url: url,
statusCode: 200,

View File

@@ -113,7 +113,7 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
text.append(
Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self)
.initWithString(
"\nFallback: \(description)",
"\n\("fallback_attribute".localized): \(description)",
typeStyle: typeStyle,
attributes: attributes
)
@@ -124,7 +124,7 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
text.append(
Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self)
.initWithString(
"\nRomanized",
"\n\("romanized_attribute".localized)",
typeStyle: typeStyle,
attributes: attributes
)
@@ -182,10 +182,9 @@ private func loadLyricsForCurrentTrack() throws {
case .InvalidMusixmatchToken:
if !hasShownUnauthorizedPopUp {
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.",
message: "musixmatch_unauthorized_popup".localized,
buttonText: "OK"
)
@@ -195,10 +194,9 @@ private func loadLyricsForCurrentTrack() throws {
case .MusixmatchRestricted:
if !hasShownRestrictedPopUp {
PopUpHelper.showPopUp(
delayed: false,
message: "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.",
message: "musixmatch_restricted_popup".localized,
buttonText: "OK"
)

View File

@@ -18,10 +18,10 @@ struct LyricsDto {
if lines.isEmpty {
lyricsData.lines = [
LyricsLine.with {
$0.content = "This song is instrumental."
$0.content = "song_is_instrumental".localized
},
LyricsLine.with {
$0.content = "Let the music play..."
$0.content = "let_the_music_play".localized
},
LyricsLine.with {
$0.content = ""

View File

@@ -10,12 +10,12 @@ enum LyricsError: Error, CustomStringConvertible {
var description: String {
switch self {
case .NoSuchSong: "No Song Found"
case .MusixmatchRestricted: "Restricted"
case .InvalidMusixmatchToken: "Unauthorized"
case .DecodingError: "Decoding Error"
case .NoCurrentTrack: "No Track Instance"
case .UnknownError: "Unknown Error"
case .NoSuchSong: "no_such_song".localized
case .MusixmatchRestricted: "musixmatch_restricted".localized
case .InvalidMusixmatchToken: "invalid_musixmatch_token".localized
case .DecodingError: "decoding_error".localized
case .NoCurrentTrack: "no_current_track".localized
case .UnknownError: "unknown_error".localized
}
}
}

View File

@@ -187,8 +187,8 @@ class MusixmatchLyricsRepository: LyricsRepository {
let subtitleTranslatedBody = subtitleTranslated["subtitle_body"] as? String,
let subtitlesTranslated = try? JSONDecoder().decode(
[MusixmatchSubtitle].self, from: subtitleTranslatedBody.data(using: .utf8)!
) {
)
{
if selectedLanguage == romanizationLanguage {
romanized = true
@@ -206,12 +206,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
}
}
if options.musixmatchLanguage.isEmpty
&& options.romanization
&& selectedLanguage != romanizationLanguage {
selectedLanguage = romanizationLanguage
if options.romanization && selectedLanguage != romanizationLanguage {
if let translations = try? getTranslations(
query.spotifyTrackId,
selectedLanguage: romanizationLanguage

View File

@@ -2,10 +2,17 @@ import Foundation
import NaturalLanguage
extension String {
static func ~= (lhs: String, rhs: String) -> Bool {
lhs.firstMatch(rhs) != nil
}
var localized: String {
BundleHelper.shared.localizedString(self)
}
func localizeWithFormat(_ arguments: CVarArg...) -> String{
String(format: self.localized, arguments: arguments)
}
var range: NSRange {
NSRange(self.startIndex..., in: self)

View File

@@ -88,7 +88,7 @@ extension UserDefaults {
static var patchType: PatchType {
get {
if let rawValue = defaults.object(forKey: patchTypeKey) as? Int {
return PatchType(rawValue: rawValue)!
return PatchType(rawValue: rawValue) ?? .requests
}
return .notSet

View File

@@ -3,14 +3,46 @@ import Orion
private 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"
message: "have_premium_popup".localized,
buttonText: "ok".localized
)
}
class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDelegate {
static var targetName: String {
EeveeSpotify.isOldSpotifyVersion
? "SPTCoreURLSessionDataDelegate"
: "SPTDataLoaderService"
}
static let targetName = "SPTCoreURLSessionDataDelegate"
func URLSession(
_ session: URLSession,
dataTask task: URLSessionDataTask,
didReceiveResponse response: HTTPURLResponse,
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
) {
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 url = request.url
else {
return
}
if url.isBootstrap {
URLSessionHelper.shared.setOrAppend(data, for: url)
return
}
orig.URLSession(session, dataTask: task, didReceiveData: data)
}
func URLSession(
_ session: URLSession,
@@ -23,16 +55,14 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
else {
return
}
if error == nil && url.isBootstrap {
let buffer = URLSessionHelper.shared.obtainData(for: url)!
do {
var bootstrapMessage = try BootstrapMessage(serializedData: buffer)
if UserDefaults.patchType == .notSet {
if bootstrapMessage.attributes["type"]?.stringValue == "premium" {
UserDefaults.patchType = .disabled
showHavePremiumPopUp()
@@ -46,7 +76,6 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
}
if UserDefaults.patchType == .requests {
modifyRemoteConfiguration(&bootstrapMessage.ucsResponse)
orig.URLSession(
@@ -71,24 +100,4 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
func URLSession(
_ session: URLSession,
dataTask task: URLSessionDataTask,
didReceiveData data: Data
) {
guard
let request = task.currentRequest,
let url = request.url
else {
return
}
if url.isBootstrap {
URLSessionHelper.shared.setOrAppend(data, for: url)
return
}
orig.URLSession(session, dataTask: task, didReceiveData: data)
}
}

View File

@@ -1,7 +1,6 @@
import Foundation
func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
if UserDefaults.overwriteConfiguration {
configuration.resolve.configuration = try! BundleHelper.shared.resolveConfiguration()
}
@@ -10,7 +9,6 @@ func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
}
func modifyAttributes(_ attributes: inout [String: AccountAttribute]) {
attributes["type"] = AccountAttribute.with {
$0.stringValue = "premium"
}

View File

@@ -3,38 +3,32 @@ import SwiftUI
import libroot
class BundleHelper {
private let bundleName = "EeveeSpotify"
private let bundle: Bundle
static let shared = BundleHelper()
private init() {
private init() {
self.bundle = Bundle(
path: Bundle.main.path(
forResource: bundleName,
forResource: bundleName,
ofType: "bundle"
)
)
?? jbRootPath("/Library/Application Support/\(bundleName).bundle")
)!
}
func uiImage(_ name: String) -> UIImage {
return UIImage(
contentsOfFile: self.bundle.path(
forResource: name,
ofType: "png"
)!
)!
}
func premiumBlankData() throws -> Data {
return try Data(
contentsOf: self.bundle.url(
forResource: "premiumblank",
withExtension: "bnk"
func uiImage(_ name: String) -> UIImage {
return UIImage(
contentsOfFile: self.bundle.path(
forResource: name,
ofType: "png"
)!
)
)!
}
func localizedString(_ key: String) -> String {
return bundle.localizedString(forKey: key, value: nil, table: nil)
}
func resolveConfiguration() throws -> ResolveConfiguration {

View File

@@ -1,73 +1,33 @@
import Foundation
class OfflineHelper {
static let persistentCachePath = FileManager.default.urls(
static private let applicationSupportPath = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
)
.first!
.appendingPathComponent("PersistentCache")
//
static var offlineBnkPath: URL {
persistentCachePath.appendingPathComponent("offline.bnk")
}
static var eeveeBnkPath: URL {
persistentCachePath.appendingPathComponent("eevee.bnk")
}
static var offlineBnkData: Data {
get throws { try Data(contentsOf: offlineBnkPath) }
}
static var eeveeBnkData: Data {
get throws { try Data(contentsOf: eeveeBnkPath) }
}
//
private static func writeOfflineBnkData(_ data: Data) throws {
try data.write(to: offlineBnkPath)
}
private static func writeEeveeBnkData(_ data: Data) throws {
try data.write(to: eeveeBnkPath)
}
//
static func restoreFromEeveeBnk() throws {
try writeOfflineBnkData(try eeveeBnkData)
}
static func backupToEeveeBnk() throws {
try writeEeveeBnkData(try offlineBnkData)
}
static func patchOfflineBnk() throws {
let fileData = try offlineBnkData
let usernameLength = Int(fileData[8])
let username = Data(fileData[9..<9 + usernameLength])
var blankData = try BundleHelper.shared.premiumBlankData()
blankData.insert(UInt8(usernameLength), at: 8)
blankData.insert(contentsOf: username, at: 9)
try writeOfflineBnkData(blankData)
}
//
static func resetPersistentCache() throws {
static private let persistentCachePath = applicationSupportPath
.appendingPathComponent("PersistentCache")
static private let remoteConfigPath = applicationSupportPath
.appendingPathComponent("remote-config")
//
static private func resetPersistentCache() throws {
try FileManager.default.removeItem(at: self.persistentCachePath)
}
static func resetOfflineBnk() throws {
try FileManager.default.removeItem(at: self.offlineBnkPath)
static private func resetRemoteConfig() throws {
try FileManager.default.removeItem(at: self.remoteConfigPath)
}
//
static func resetData() {
try? resetPersistentCache()
try? resetRemoteConfig()
}
}

View File

@@ -3,10 +3,7 @@ import Foundation
enum PatchType: Int {
case notSet
case disabled
case offlineBnk
case requests
var isPatching: Bool {
self == .requests || self == .offlineBnk
}
var isPatching: Bool { self == .requests }
}

View File

@@ -0,0 +1,22 @@
import Foundation
protocol SpotifySessionDelegate {
func URLSession(
_ session: URLSession,
task: URLSessionDataTask,
didCompleteWithError error: Error?
)
func URLSession(
_ session: URLSession,
dataTask task: URLSessionDataTask,
didReceiveData data: Data
)
func URLSession(
_ session: URLSession,
dataTask task: URLSessionDataTask,
didReceiveResponse response: HTTPURLResponse,
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
)
}

View File

@@ -1,48 +0,0 @@
import Foundation
import UIKit
class OfflineObserver: NSObject, NSFilePresenter {
var presentedItemURL: URL?
var presentedItemOperationQueue: OperationQueue
override init() {
presentedItemURL = OfflineHelper.offlineBnkPath
presentedItemOperationQueue = .main
}
func presentedItemDidChange() {
let productState = HookedInstances.productState!
if productState.stringForKey("type") == "premium" {
// if productState.stringForKey("shuffle") == "0" {
// return
// }
do {
try OfflineHelper.backupToEeveeBnk()
NSLog("[EeveeSpotify] Settings has changed, updated eevee.bnk")
}
catch {
NSLog("[EeveeSpotify] Unable to update eevee.bnk: \(error)")
}
return
}
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 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: {
exitApplication()
},
onSecondaryClick: {
try! OfflineHelper.resetPersistentCache()
exitApplication()
}
)
}
}

View File

@@ -4,15 +4,13 @@ import UIKit
struct ServerSidedReminder: HookGroup { }
class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
typealias Group = ServerSidedReminder
static let targetName = "StreamQualitySettingsSection"
func shouldResetSelection() -> Bool {
PopUpHelper.showPopUp(
message: "Very high audio quality is server-sided and is not available with this tweak.",
buttonText: "OK"
message: "high_audio_quality_popup".localized,
buttonText: "ok".localized
)
return true
@@ -23,13 +21,12 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
private func showOfflineModePopUp() {
PopUpHelper.showPopUp(
message: "Native playlist downloading is server-sided and is not available with this tweak. You can download podcast episodes though.",
buttonText: "OK"
message: "playlist_downloading_popup".localized,
buttonText: "ok".localized
)
}
class FTPDownloadActionHook: ClassHook<NSObject> {
typealias Group = ServerSidedReminder
static let targetName = "ListUXPlatform_FreeTierPlaylistImpl.FTPDownloadAction"
@@ -39,7 +36,6 @@ class FTPDownloadActionHook: ClassHook<NSObject> {
}
class UIButtonHook: ClassHook<UIButton> {
typealias Group = ServerSidedReminder
func setHighlighted(_ highlighted: Bool) {

View File

@@ -1,9 +1,7 @@
import SwiftUI
extension EeveeSettingsView {
func loadVersion() async throws {
let (data, _) = try await URLSession.shared.data(
from: URL(string: "https://api.github.com/repos/whoeevee/EeveeSpotify/releases/latest")!
)
@@ -24,11 +22,10 @@ extension EeveeSettingsView {
}
@ViewBuilder func VersionSection() -> some View {
Section {
if isUpdateAvailable {
Link(
"Update Available",
"update_available".localized,
destination: URL(string: "https://github.com/whoeevee/EeveeSpotify/releases")!
)
}
@@ -39,7 +36,7 @@ extension EeveeSettingsView {
if latestVersion.isEmpty {
HStack(spacing: 10) {
ProgressView()
Text("Checking for Update...")
Text("checking_for_update".localized)
}
}
}

View File

@@ -2,7 +2,6 @@ import SwiftUI
import UIKit
struct EeveeSettingsView: View {
let navigationController: UINavigationController
@State var latestVersion = ""
@@ -19,7 +18,6 @@ struct EeveeSettingsView: View {
var body: some View {
List {
VersionSection()
if !hasShownCommonIssuesTip {
@@ -34,43 +32,52 @@ struct EeveeSettingsView: View {
//
Button {
pushSettingsController(with: EeveePatchingSettingsView(), title: "Patching")
pushSettingsController(
with: EeveePatchingSettingsView(),
title: "patching".localized
)
} label: {
NavigationSectionView(
color: .orange,
title: "Patching",
title: "patching".localized,
imageSystemName: "hammer.fill"
)
}
Button {
pushSettingsController(with: EeveeLyricsSettingsView(), title: "Lyrics")
pushSettingsController(
with: EeveeLyricsSettingsView(),
title: "lyrics".localized
)
} label: {
NavigationSectionView(
color: .blue,
title: "Lyrics",
title: "lyrics".localized,
imageSystemName: "quote.bubble.fill"
)
}
Button {
pushSettingsController(with: EeveeUISettingsView(), title: "Customization")
pushSettingsController(
with: EeveeUISettingsView(),
title: "customization".localized
)
} label: {
NavigationSectionView(
color: Color(hex: "#64D2FF"),
title: "Customization",
title: "customization".localized,
imageSystemName: "paintpalette.fill"
)
}
//
Section(footer: Text("Clear cached data and restart the app.")) {
Section(footer: Text("reset_data_description".localized)) {
Button {
try! OfflineHelper.resetPersistentCache()
OfflineHelper.resetData()
exitApplication()
} label: {
Text("Reset Data")
Text("reset_data".localized)
}
}
}

View File

@@ -3,7 +3,6 @@ import SwiftUI
extension EeveeLyricsSettingsView {
func getMusixmatchToken(_ input: String) -> String? {
if let match = input.firstMatch("\\[UserToken\\]: ([a-f0-9]+)"),
let tokenRange = Range(match.range(at: 1), in: input) {
return String(input[tokenRange])
@@ -16,10 +15,9 @@ extension EeveeLyricsSettingsView {
}
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.",
title: "enter_user_token".localized,
message: "enter_user_token_message".localized,
preferredStyle: .alert
)
@@ -29,11 +27,11 @@ extension EeveeLyricsSettingsView {
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { _ in
lyricsSource = oldSource
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { _ in
let text = alert.textFields!.first!.text!
guard let token = getMusixmatchToken(text) else {

View File

@@ -3,26 +3,15 @@ import SwiftUI
extension EeveeLyricsSettingsView {
func lyricsSourceFooter() -> some View {
var 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.
"""
var text = "lyrics_source_description".localized
if Locale.isInRegion("JP", orHasLanguage: "ja") {
text.append("\n\n")
text.append("PetitLyrics: Offers plenty of time-synced Japanese and some international lyrics.")
text.append("petitlyrics_description".localized)
}
text.append("\n\n")
text.append("""
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 when using Genius due to how the tweak searches songs. I've made it work in most cases.
""")
text.append("lyrics_additional_info".localized)
return Text(text)
}
@@ -30,7 +19,7 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
@ViewBuilder func LyricsSourceSection() -> some View {
Section(footer: lyricsSourceFooter()) {
Picker(
"Lyrics Source",
"lyrics_source".localized,
selection: $lyricsSource
) {
Text("Genius").tag(LyricsSource.genius)
@@ -45,9 +34,9 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
VStack(alignment: .leading, spacing: 5) {
Text("Musixmatch User Token")
Text("musixmatch_user_token".localized)
TextField("Enter User Token or Paste Debug Info", text: $musixmatchToken)
TextField("user_token_placeholder".localized, text: $musixmatchToken)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -55,8 +44,9 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
}
.onChange(of: musixmatchToken) { input in
if input.isEmpty { return }
if input.isEmpty {
return
}
if let token = getMusixmatchToken(input) {
UserDefaults.musixmatchToken = token
@@ -68,7 +58,6 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource)
return

View File

@@ -11,21 +11,20 @@ struct EeveeLyricsSettingsView: View {
var body: some View {
List {
LyricsSourceSection()
if lyricsSource != .genius {
Section(
footer: Text("Load lyrics from Genius if there is a problem with \(lyricsSource).")
footer: Text("genius_fallback_description".localizeWithFormat(lyricsSource.description))
) {
Toggle(
"Genius Fallback",
"genius_fallback".localized,
isOn: $geniusFallback
)
if geniusFallback {
Toggle(
"Show Fallback Reasons",
"show_fallback_reasons".localized,
isOn: Binding<Bool>(
get: { UserDefaults.fallbackReasons },
set: { UserDefaults.fallbackReasons = $0 }
@@ -37,9 +36,9 @@ struct EeveeLyricsSettingsView: View {
//
Section(footer: Text("Display romanized lyrics for Japanese, Korean, and Chinese.")) {
Section(footer: Text("romanized_lyrics_description".localized)) {
Toggle(
"Romanized Lyrics",
"romanized_lyrics".localized,
isOn: $lyricsOptions.romanization
)
}
@@ -53,7 +52,7 @@ struct EeveeLyricsSettingsView: View {
.foregroundColor(.yellow)
}
Text("Musixmatch Lyrics Language")
Text("musixmatch_language".localized)
Spacer()
@@ -62,7 +61,7 @@ struct EeveeLyricsSettingsView: View {
.foregroundColor(.gray)
}
} footer: {
Text("You can enter a 2-letter Musixmatch language code and see translated lyrics on Musixmatch if they are available.")
Text("musixmatch_language_description".localized)
}
}
@@ -85,7 +84,6 @@ struct EeveeLyricsSettingsView: View {
}
.onChange(of: lyricsOptions) { lyricsOptions in
let selectedLanguage = lyricsOptions.musixmatchLanguage
if selectedLanguage.isEmpty || selectedLanguage ~= "^[\\w\\d]{2}$" {

View File

@@ -7,64 +7,32 @@ struct EeveePatchingSettingsView: 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.
""")) {
Section(footer: patchType == .disabled ? nil : Text("patching_description".localized)) {
Toggle(
"Do Not Patch Premium",
"do_not_patch_premium".localized,
isOn: Binding<Bool>(
get: { patchType == .disabled },
set: { patchType = $0 ? .disabled : .requests }
)
)
if patchType != .disabled {
Picker(
"Patching Method",
selection: $patchType
) {
Text("Static").tag(PatchType.offlineBnk)
Text("Dynamic").tag(PatchType.requests)
}
}
}
.onChange(of: patchType) { newPatchType in
UserDefaults.patchType = newPatchType
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
OfflineHelper.resetData()
}
.onChange(of: overwriteConfiguration) { overwriteConfiguration in
UserDefaults.overwriteConfiguration = overwriteConfiguration
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
OfflineHelper.resetData()
}
if patchType == .requests {
Section(
footer: Text("Replace remote configuration with the dumped Premium one. It might fix some issues, such as appearing ads, but it's not guaranteed.")
footer: Text("overwrite_configuration_description".localized)
) {
Toggle(
"Overwrite Configuration",
"overwrite_configuration".localized,
isOn: $overwriteConfiguration
)
}

View File

@@ -2,31 +2,26 @@ import SwiftUI
import UIKit
struct EeveeUISettingsView: View {
@State var lyricsColors = UserDefaults.lyricsColors
var body: some View {
List {
Section(
header: Text("Lyrics Background Color"),
footer: Text("""
If you turn on Display Original Colors, the lyrics will appear in the original Spotify colors for tracks that have them.
You can set a static color or a normalization factor based on the extracted track cover's color. This factor determines how much dark colors are lightened and light colors are darkened. Generally, you will see lighter colors with a higher normalization factor.
""")) {
header: Text("lyrics_background_color_section".localized),
footer: Text("lyrics_background_color_section_description".localized)) {
Toggle(
"Display Original Colors",
"display_original_colors".localized,
isOn: $lyricsColors.displayOriginalColors
)
Toggle(
"Use Static Color",
"use_static_color".localized,
isOn: $lyricsColors.useStaticColor
)
if lyricsColors.useStaticColor {
ColorPicker(
"Static Color",
"static_color".localized,
selection: Binding<Color>(
get: { Color(hex: lyricsColors.staticColor) },
set: { lyricsColors.staticColor = $0.hexString }
@@ -36,8 +31,7 @@ struct EeveeUISettingsView: View {
}
else {
VStack(alignment: .leading, spacing: 5) {
Text("Color Normalization Factor")
Text("color_normalization_factor".localized)
Slider(
value: $lyricsColors.normalizationFactor,
@@ -54,7 +48,7 @@ struct EeveeUISettingsView: View {
Section {
Toggle(
"Dark PopUps",
"dark_popups".localized,
isOn: Binding<Bool>(
get: { UserDefaults.darkPopUps },
set: { UserDefaults.darkPopUps = $0 }

View File

@@ -2,7 +2,6 @@ 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)
@@ -15,78 +14,12 @@ struct EeveeSpotify: Tweak {
static let isOldSpotifyVersion = NSClassFromString("Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController") == nil
init() {
do {
defer {
if UserDefaults.darkPopUps {
DarkPopUps().activate()
}
let patchType = UserDefaults.patchType
if patchType.isPatching {
if patchType == .offlineBnk {
NSFileCoordinator.addFilePresenter(OfflineObserver())
}
ServerSidedReminder().activate()
}
}
switch UserDefaults.patchType {
case .disabled:
NSLog("[EeveeSpotify] Not activating: patchType is disabled")
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"
)
}
case .notSet:
NSLog("[EeveeSpotify] Patching method not set, resetting offline.bnk")
try? OfflineHelper.resetOfflineBnk()
default:
break
}
if UserDefaults.darkPopUps {
DarkPopUps().activate()
}
catch {
NSLog("[EeveeSpotify] Unable to apply tweak: \(error)")
PopUpHelper.showPopUp(
delayed: true,
message: "Unable to apply tweak: \(error)",
buttonText: "OK"
)
if UserDefaults.patchType.isPatching {
ServerSidedReminder().activate()
}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
/* MARK: Settings */
patching = "Patching";
lyrics = "Lyrics";
customization = "Customization";
reset_data = "Reset Data";
reset_data_description = "Clear cached data and restart the app.";
checking_for_update = "Checking for Update...";
update_available = "Update Available";
cancel = "Cancel";
ok = "OK";
// Patching
do_not_patch_premium = "Do Not Patch Premium";
patching_description = "The tweak intercepts requests to load user data, deserializes it, and modifies the parameters in real-time.
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. App restart is required after changing.";
overwrite_configuration = "Overwrite Configuration";
overwrite_configuration_description = "Replace remote configuration with the dumped Premium one. It might fix some issues, such as appearing ads, but it's not guaranteed.";
// Lyrics
lyrics_source = "Lyrics Source";
lyrics_source_description = "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.";
lyrics_additional_info = "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 when using Genius due to how the tweak searches songs. I've made it work in most cases.";
petitlyrics_description = "PetitLyrics: Offers plenty of time-synced Japanese and some international lyrics.";
musixmatch_user_token = "Musixmatch User Token";
user_token_placeholder = "Enter User Token or Paste Debug Info";
enter_user_token = "Enter User Token";
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.";
genius_fallback = "Genius Fallback";
genius_fallback_description = "Load lyrics from Genius if there is a problem with %@.";
show_fallback_reasons = "Show Fallback Reasons";
romanized_lyrics = "Romanized Lyrics";
romanized_lyrics_description = "Display romanized lyrics for Japanese, Korean, and Chinese.";
musixmatch_language = "Musixmatch Lyrics Language";
musixmatch_language_description = "You can enter a 2-letter Musixmatch language code and see translated lyrics on Musixmatch if they are available.";
// UI
lyrics_background_color_section = "Lyrics Background Color";
lyrics_background_color_section_description = "If you turn on Display Original Colors, the lyrics will appear in the original Spotify colors for tracks that have them.
You can set a static color or a normalization factor based on the extracted track cover's color. This factor determines how much dark colors are lightened and light colors are darkened. Generally, you will see lighter colors with a higher normalization factor.";
display_original_colors = "Display Original Colors";
use_static_color = "Use Static Color";
static_color = "Static Color";
color_normalization_factor = "Color Normalization Factor";
dark_popups = "Dark PopUps";
/* MARK: Premium PopUps */
have_premium_popup = "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.";
high_audio_quality_popup = "Very high audio quality is server-sided and is not available with this tweak.";
playlist_downloading_popup = "Native playlist downloading is server-sided and is not available with this tweak. You can download podcast episodes though.";
/* MARK: Lyrics */
fallback_attribute = "Fallback";
romanized_attribute = "Romanized";
musixmatch_unauthorized_popup = "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.";
musixmatch_restricted_popup = "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.";
// Errors Titles
no_such_song = "No Song Found";
musixmatch_restricted = "Restricted";
invalid_musixmatch_token = "Unauthorized";
decoding_error = "Decoding Error";
no_current_track = "No Track Instance";
unknown_error = "Unknown Error";
// Instrumental Titles
song_is_instrumental = "This song is instrumental.";
let_the_music_play = "Let the music play...";