better URLSession intercepts/fix #182, lyrics colors options

This commit is contained in:
eevee
2024-06-11 01:16:55 +03:00
parent b2411b344c
commit a71253a3b7
14 changed files with 339 additions and 230 deletions
@@ -5,17 +5,71 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
static let targetName = "SPTDataLoaderService"
// orion:new
func shouldModify(_ url: URL) -> Bool {
let isModifyingCustomizeResponse = UserDefaults.patchType == .requests
return url.isLyrics || (url.isCustomize && isModifyingCustomizeResponse)
}
func URLSession(
_ session: URLSession,
task: URLSessionDataTask,
didCompleteWithError error: Error?
) {
if let url = task.currentRequest?.url {
if url.isLyrics || (UserDefaults.patchType == .requests && url.isCustomize) {
return
guard
let request = task.currentRequest,
let url = request.url
else {
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 getCurrentTrackLyricsData(
originalLyrics: try? Lyrics(serializedData: buffer)
)
)
orig.URLSession(session, task: task, didCompleteWithError: nil)
}
catch {
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)")
}
}
}
orig.URLSession(session, task: task, didCompleteWithError: error)
}
func URLSession(
@@ -25,7 +79,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
completionHandler handler: Any
) {
let url = response.url!
if url.isLyrics, response.statusCode != 200 {
let okResponse = HTTPURLResponse(
@@ -34,17 +88,17 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
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)
@@ -73,63 +127,14 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
) {
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(
originalLyrics: try? Lyrics(serializedData: data)
)
)
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)
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)")
}
if shouldModify(url) {
URLSessionHelper.shared.setOrAppend(data, for: url)
return
}
orig.URLSession(session, dataTask: task, didReceiveData: data)
@@ -0,0 +1,23 @@
import UIKit
class URLSessionHelper {
static let shared = URLSessionHelper()
private var requestsMap: [URL:Data]
private init() {
self.requestsMap = [:]
}
func setOrAppend(_ data: Data, for url: URL) {
var loadedData = requestsMap[url] ?? Data()
loadedData.append(data)
requestsMap[url] = loadedData
}
func obtainData(for url: URL) -> Data? {
return requestsMap.removeValue(forKey: url)
}
}
@@ -5,11 +5,11 @@ class SPTPlayerTrackHook: ClassHook<NSObject> {
static let targetName = "SPTPlayerTrack"
func setMetadata(_ metadata: [String:String]) {
var meta = metadata
func metadata() -> [String:String] {
var meta = orig.metadata()
meta["has_lyrics"] = "true"
orig.setMetadata(meta)
return meta
}
}
@@ -29,24 +29,24 @@ class EncoreButtonHook: ClassHook<UIButton> {
}
func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) 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(),
title: track.trackTitle(),
artist: track.artistTitle(),
spotifyTrackId: track.URI().spt_trackIdentifier(),
source: source
)
}
catch let error as LyricsError {
switch error {
@@ -67,10 +67,10 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data {
if source == .genius || !UserDefaults.geniusFallback {
throw error
}
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(),
@@ -80,13 +80,28 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data {
}
let lyrics = try Lyrics.with {
$0.colors = originalLyrics?.colors ?? LyricsColors.with {
$0.backgroundColor = Color(hex: track.extractedColorHex()).normalized.uInt32
$0.lineColor = Color.black.uInt32
$0.activeLineColor = Color.white.uInt32
}
$0.colors = getLyricsColors()
$0.data = try LyricsHelper.composeLyricsData(plainLyrics!, source: source)
}
return try lyrics.serializedData()
func getLyricsColors() -> LyricsColors {
let lyricsColorsSettings = UserDefaults.lyricsColors
if lyricsColorsSettings.displayOriginalColors, let originalLyrics = originalLyrics {
return originalLyrics.colors
}
return LyricsColors.with {
$0.backgroundColor = lyricsColorsSettings.useStaticColor
? Color(hex: lyricsColorsSettings.staticColor).uInt32
: Color(hex: track.extractedColorHex())
.normalized(lyricsColorsSettings.normalizationFactor)
.uInt32
$0.lineColor = Color.black.uInt32
$0.activeLineColor = Color.white.uInt32
}
}
}
@@ -54,10 +54,6 @@ class LyricsHelper {
captures[name] = String(line[substringRange])
}
}
if captures.count != 3 {
return nil
}
let minute = Int(captures["minute"]!)!
let seconds = Float(captures["seconds"]!)!
@@ -0,0 +1,8 @@
import Foundation
struct LyricsColorsSettings: Codable, Equatable {
var displayOriginalColors: Bool
var useStaticColor: Bool
var staticColor: String
var normalizationFactor: CGFloat
}
@@ -53,10 +53,19 @@ extension Color {
) / 1000
}
var normalized: Color {
brightness < 0.5
? self.lighter(by: 0.5 - brightness)
: self.darker(by: brightness - 0.5)
func normalized(_ by: CGFloat) -> Color {
brightness < 0.5
? self.lighter(by: max(by - brightness, 0))
: self.darker(by: max(brightness - by, 0))
}
var hexString: String {
String(
format: "%02X%02X%02X",
Int(components.red * 255),
Int(components.green * 255),
Int(components.blue * 255)
)
}
var uInt32: UInt32 {
@@ -10,6 +10,7 @@ extension UserDefaults {
private static let darkPopUpsKey = "darkPopUps"
private static let patchTypeKey = "patchType"
private static let overwriteConfigurationKey = "overwriteConfiguration"
private static let lyricsColorsKey = "lyricsColors"
static var lyricsSource: LyricsSource {
get {
@@ -72,4 +73,22 @@ extension UserDefaults {
defaults.set(overwriteConfiguration, forKey: overwriteConfigurationKey)
}
}
static var lyricsColors: LyricsColorsSettings {
get {
if let data = defaults.object(forKey: lyricsColorsKey) as? Data {
return try! JSONDecoder().decode(LyricsColorsSettings.self, from: data)
}
return LyricsColorsSettings(
displayOriginalColors: true,
useStaticColor: false,
staticColor: "",
normalizationFactor: 0.5
)
}
set (lyricsColors) {
defaults.set(try! JSONEncoder().encode(lyricsColors), forKey: lyricsColorsKey)
}
}
}
@@ -1,6 +1,6 @@
import Orion
func showHavePremiumPopUp() {
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.",
@@ -8,18 +8,6 @@ func showHavePremiumPopUp() {
)
}
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()
}
)
}
class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
static let targetName = "SPTCoreURLSessionDataDelegate"
@@ -29,37 +17,18 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
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
guard
let request = task.currentRequest,
let response = task.response,
let url = request.url
else {
return
}
if url.isBootstrap {
if error == nil && url.isBootstrap {
let buffer = URLSessionHelper.shared.obtainData(for: url)!
do {
guard let buffer = OfflineHelper.appendDataAndReturnIfFull(
data,
response: response
) else {
return
}
OfflineHelper.dataBuffer = Data()
var bootstrapMessage = try BootstrapMessage(serializedData: buffer)
if UserDefaults.patchType == .notSet {
@@ -98,6 +67,28 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
NSLog("[EeveeSpotify] Unable to modify bootstrap data: \(error)")
}
}
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
}
let isModifyingBootstrapResponse = UserDefaults.patchType == .requests
if url.isBootstrap && isModifyingBootstrapResponse {
URLSessionHelper.shared.setOrAppend(data, for: url)
return
}
orig.URLSession(session, dataTask: task, didReceiveData: data)
}
@@ -26,21 +26,6 @@ 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 {
@@ -21,7 +21,7 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
//
func showOfflineModePopUp() {
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"
@@ -0,0 +1,53 @@
import SwiftUI
extension EeveeSettingsView {
@ViewBuilder func LyricsColorsSection() -> some View {
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.
""")) {
Toggle(
"Display Original Colors",
isOn: $lyricsColors.displayOriginalColors
)
Toggle(
"Use Static Color",
isOn: $lyricsColors.useStaticColor
)
if lyricsColors.useStaticColor {
ColorPicker(
"Static Color",
selection: Binding<Color>(
get: { Color(hex: lyricsColors.staticColor) },
set: { lyricsColors.staticColor = $0.hexString }
),
supportsOpacity: false
)
}
else {
VStack(alignment: .leading, spacing: 5) {
Text("Color Normalization Factor")
Slider(
value: $lyricsColors.normalizationFactor,
in: 0.2...0.8,
step: 0.1
)
}
}
}
.onChange(of: lyricsColors) { newLyricsColors in
UserDefaults.lyricsColors = newLyricsColors
}
}
}
@@ -32,6 +32,30 @@ If you have an active Premium subscription, you can turn on Do Not Patch Premium
}
}
.onChange(of: patchType) { newPatchType in
UserDefaults.patchType = newPatchType
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
}
.onChange(of: overwriteConfiguration) { overwriteConfiguration in
UserDefaults.overwriteConfiguration = overwriteConfiguration
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
}
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.")
@@ -7,50 +7,7 @@ struct EeveeSettingsView: View {
@State var patchType = UserDefaults.patchType
@State var lyricsSource = UserDefaults.lyricsSource
@State var overwriteConfiguration = UserDefaults.overwriteConfiguration
private 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])
}
else if input ~= "^[a-f0-9]+$" {
return input
}
return nil
}
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!
guard let token = getMusixmatchToken(text) else {
lyricsSource = oldSource
return
}
musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
@State var lyricsColors = UserDefaults.lyricsColors
var body: some View {
@@ -59,6 +16,8 @@ struct EeveeSettingsView: View {
PremiumSections()
LyricsSections()
LyricsColorsSection()
Section {
Toggle(
@@ -87,54 +46,8 @@ struct EeveeSettingsView: View {
.animation(.default, value: lyricsSource)
.animation(.default, value: patchType)
.onChange(of: musixmatchToken) { input in
if input.isEmpty { return }
if let token = getMusixmatchToken(input) {
UserDefaults.musixmatchToken = token
self.musixmatchToken = token
}
else {
self.musixmatchToken = ""
}
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource)
return
}
UserDefaults.lyricsSource = newSource
}
.animation(.default, value: lyricsColors)
.onChange(of: patchType) { newPatchType in
UserDefaults.patchType = newPatchType
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
}
.onChange(of: overwriteConfiguration) { overwriteConfiguration in
UserDefaults.overwriteConfiguration = overwriteConfiguration
do {
try OfflineHelper.resetOfflineBnk()
}
catch {
NSLog("Unable to reset offline.bnk: \(error)")
}
}
.onAppear {
UIView.appearance(
whenContainedInInstancesOf: [UIAlertController.self]
@@ -2,6 +2,51 @@ import SwiftUI
extension EeveeSettingsView {
private 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])
}
else if input ~= "^[a-f0-9]+$" {
return input
}
return nil
}
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!
guard let token = getMusixmatchToken(text) else {
lyricsSource = oldSource
return
}
musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
@ViewBuilder func LyricsSections() -> some View {
Section(footer: Text("""
@@ -36,6 +81,29 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.onChange(of: musixmatchToken) { input in
if input.isEmpty { return }
if let token = getMusixmatchToken(input) {
UserDefaults.musixmatchToken = token
self.musixmatchToken = token
}
else {
self.musixmatchToken = ""
}
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource)
return
}
UserDefaults.lyricsSource = newSource
}
if lyricsSource != .genius {
Section(