what i've done so far

This commit is contained in:
eevee
2025-06-17 19:25:23 +03:00
parent 727fd664ef
commit 92e6cc7900
47 changed files with 298 additions and 271 deletions
+6 -3
View File
@@ -4,9 +4,12 @@ import SwiftUI
struct DarkPopUps: HookGroup { }
private let popUpContainerViewController = EeveeSpotify.isOldSpotifyVersion
? "SPTEncorePopUpContainer"
: "EncoreConsumerMobile_Wrappers.PopUpPresentableContainer"
private var popUpContainerViewController: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "SPTEncorePopUpContainer"
default: return "EncoreConsumerMobile_Wrappers.PopUpPresentableContainer"
}
}
class EncoreLabelHook: ClassHook<UIView> {
typealias Group = DarkPopUps
@@ -34,7 +34,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
orig.URLSession(
session,
dataTask: task,
didReceiveData: try getLyricsForCurrentTrack(
didReceiveData: try getLyricsDataForCurrentTrack(
originalLyrics: try? Lyrics(serializedBytes: buffer)
)
)
@@ -94,7 +94,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
)!
do {
let lyricsData = try getLyricsForCurrentTrack()
let lyricsData = try getLyricsDataForCurrentTrack()
orig.URLSession(
session,
@@ -109,9 +109,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
return
}
catch {
NSLog("[EeveeSpotify] Unable to load lyrics: \(error)")
orig.URLSession(session, task: task, didCompleteWithError: error)
return
}
}
@@ -1,45 +0,0 @@
import Orion
import UIKit
class HookedInstances {
static var productState: SPTCoreProductState?
static var currentTrack: SPTPlayerTrack?
static var nowPlayingMetaBackgroundModel: SPTNowPlayingMetadataBackgroundViewModel?
}
class SPTNowPlayingModelHook: ClassHook<NSObject> {
static let targetName = "SPTNowPlayingModel"
func currentTrack() -> SPTPlayerTrack? {
if let track = orig.currentTrack() {
HookedInstances.currentTrack = track
return track
}
return nil
}
}
class SPTNowPlayingMetadataBackgroundViewModelHook: ClassHook<NSObject> {
static let targetName = "SPTNowPlayingMetadataBackgroundViewModel"
func color() -> UIColor {
HookedInstances.nowPlayingMetaBackgroundModel = Dynamic.convert(
target,
to: SPTNowPlayingMetadataBackgroundViewModel.self
)
return orig.color()
}
}
class SPTCoreProductStateInstanceHook: ClassHook<NSObject> {
static let targetName = "SPTCoreProductState"
func stringForKey(_ key: String) -> NSString {
HookedInstances.productState = Dynamic.convert(
target,
to: SPTCoreProductState.self
)
return orig.stringForKey(key)
}
}
@@ -1,10 +1,11 @@
import Orion
import UIKit
class SPTPlayerTrackHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static let targetName = "SPTPlayerTrack"
func metadata() -> [String:String] {
func metadata() -> [String: String] {
var meta = orig.metadata()
meta["has_lyrics"] = "true"
@@ -14,14 +15,33 @@ class SPTPlayerTrackHook: ClassHook<NSObject> {
class LyricsScrollProviderHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.ScrollProvider"
: "Lyrics_NPVCommunicatorImpl.ScrollProvider"
}
static var targetName = HookTargetNameHelper.lyricsScrollProvider
func isEnabledForTrack(_ track: SPTPlayerTrack) -> Bool {
return true
}
}
class NowPlayingScrollViewControllerHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static var targetName = "NowPlaying_ScrollImpl.NowPlayingScrollViewController"
func nowPlayingScrollViewModelWithDidLoadComponentsFor(
_ track: SPTPlayerTrack,
withDifferentProviders: Bool,
scrollEnabledValueChanged: Bool
) -> NowPlayingScrollViewController {
var controller = orig.nowPlayingScrollViewModelWithDidLoadComponentsFor(
track,
withDifferentProviders: withDifferentProviders,
scrollEnabledValueChanged: scrollEnabledValueChanged
)
if !scrollEnabledValueChanged {
controller.scrollEnabled = true
controller.nowPlayingScrollViewModelDidChangeScrollEnabledValue()
}
return controller
}
}
@@ -0,0 +1,30 @@
import Orion
import UIKit
class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "Lyrics_CoreImpl.FullscreenViewController"
default: return "Lyrics_FullscreenPageImpl.FullscreenViewController"
}
}
func viewDidLoad() {
orig.viewDidLoad()
if UserDefaults.lyricsSource == .musixmatch
&& lyricsState.fallbackError == nil
&& !lyricsState.wasRomanized
&& !lyricsState.isEmpty {
return
}
let headerView = Ivars<UIView>(target.view).headerView
if let reportButton = headerView.subviews(matching: "EncoreButton")[1] as? UIButton {
reportButton.isEnabled = false
}
}
}
@@ -0,0 +1,100 @@
import Orion
import UIKit
class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "Lyrics_CoreImpl.LyricsOnlyViewController"
default: return "Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController"
}
}
func viewDidLoad() {
orig.viewDidLoad()
guard
let lyricsHeaderViewController = target.parent?.children.first
else {
return
}
guard let lyricsLabel = WindowHelper.shared.findFirstSubview(
"SPTEncoreLabel",
in: lyricsHeaderViewController.view
) else {
return
}
let encoreLabel = Dynamic.convert(lyricsLabel, to: SPTEncoreLabel.self)
var text = [
encoreLabel.text().firstObject
]
let attributes = Dynamic.SPTEncoreAttributes
.alloc(interface: SPTEncoreAttributes.self)
.`init`({ attributes in
attributes.setForegroundColor(.white.withAlphaComponent(0.5))
})
let typeStyle = type(
of: Dynamic[
dynamicMember: EeveeSpotify.hookTarget == .lastAvailableiOS14
? "SPTEncoreTypeStyle"
: "SPTEncoreTextStyle"
].alloc(interface: SPTEncoreTypeStyle.self)
).bodyMediumBold()
//
if UserDefaults.lyricsOptions.showFallbackReasons,
let description = lyricsState.fallbackError?.description
{
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
text.append(
EeveeSpotify.hookTarget == .lastAvailableiOS14
? attributedString.initWithString(
"\n\("fallback_attribute".localized): \(description)",
typeStyle: typeStyle,
attributes: attributes
)
: attributedString.initWithString(
"\n\("fallback_attribute".localized): \(description)",
textStyle: typeStyle,
attributes: attributes
)
)
}
if lyricsState.wasRomanized {
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
text.append(
EeveeSpotify.hookTarget == .lastAvailableiOS14
? attributedString.initWithString(
"\n\("romanized_attribute".localized)",
typeStyle: typeStyle,
attributes: attributes
)
: attributedString.initWithString(
"\n\("romanized_attribute".localized)",
textStyle: typeStyle,
attributes: attributes
)
)
}
if EeveeSpotify.hookTarget == .lastAvailableiOS14 {
encoreLabel.setNumberOfLines(text.count)
}
encoreLabel.setText(text as NSArray)
}
}
+35 -182
View File
@@ -5,163 +5,24 @@ import SwiftUI
struct LyricsGroup: HookGroup { }
//
var lyricsState = LyricsLoadingState()
class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.FullscreenViewController"
: "Lyrics_FullscreenPageImpl.FullscreenViewController"
}
func viewDidLoad() {
orig.viewDidLoad()
if UserDefaults.lyricsSource == .musixmatch
&& lastLyricsState.fallbackError == nil
&& !lastLyricsState.wasRomanized
&& !lastLyricsState.areEmpty {
return
}
let headerView = Ivars<UIView>(target.view).headerView
if let reportButton = headerView.subviews(matching: "EncoreButton")[1] as? UIButton {
reportButton.isEnabled = false
}
}
}
//
private var preloadedLyrics: Lyrics? = nil
private var lastLyricsState = LyricsLoadingState()
private var hasShownRestrictedPopUp = false
private var hasShownUnauthorizedPopUp = false
//
class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.LyricsOnlyViewController"
: "Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController"
}
func viewDidLoad() {
orig.viewDidLoad()
guard
let lyricsHeaderViewController = target.parent?.children.first
else {
return
}
//
guard let lyricsLabel = WindowHelper.shared.findFirstSubview(
"SPTEncoreLabel",
in: lyricsHeaderViewController.view
) else {
return
}
//
let encoreLabel = Dynamic.convert(lyricsLabel, to: SPTEncoreLabel.self)
var text = [
encoreLabel.text().firstObject
]
let attributes = Dynamic.SPTEncoreAttributes
.alloc(interface: SPTEncoreAttributes.self)
.`init`({ attributes in
attributes.setForegroundColor(.white.withAlphaComponent(0.5))
})
let typeStyle = type(
of: Dynamic[
dynamicMember: EeveeSpotify.isOldSpotifyVersion
? "SPTEncoreTypeStyle"
: "SPTEncoreTextStyle"
].alloc(interface: SPTEncoreTypeStyle.self)
).bodyMediumBold()
//
if UserDefaults.lyricsOptions.showFallbackReasons,
let description = lastLyricsState.fallbackError?.description
{
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
text.append(
EeveeSpotify.isOldSpotifyVersion
? attributedString.initWithString(
"\n\("fallback_attribute".localized): \(description)",
typeStyle: typeStyle,
attributes: attributes
)
: attributedString.initWithString(
"\n\("fallback_attribute".localized): \(description)",
textStyle: typeStyle,
attributes: attributes
)
)
}
if lastLyricsState.wasRomanized {
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
text.append(
EeveeSpotify.isOldSpotifyVersion
? attributedString.initWithString(
"\n\("romanized_attribute".localized)",
typeStyle: typeStyle,
attributes: attributes
)
: attributedString.initWithString(
"\n\("romanized_attribute".localized)",
textStyle: typeStyle,
attributes: attributes
)
)
}
if EeveeSpotify.isOldSpotifyVersion {
encoreLabel.setNumberOfLines(text.count)
}
encoreLabel.setText(text as NSArray)
}
}
//
var hasShownRestrictedPopUp = false
var hasShownUnauthorizedPopUp = false
private let geniusLyricsRepository = GeniusLyricsRepository()
private let petitLyricsRepository = PetitLyricsRepository()
//
private func loadLyricsForCurrentTrack() throws {
guard let track = HookedInstances.currentTrack else {
private func loadCustomLyricsForCurrentTrack() throws -> Lyrics {
guard let track = nowPlayingScrollViewController?.loadedTrack else {
throw LyricsError.noCurrentTrack
}
//
let searchQuery = LyricsSearchQuery(
title: track.trackTitle(),
primaryArtist: EeveeSpotify.isOldSpotifyVersion
primaryArtist: EeveeSpotify.hookTarget == .lastAvailableiOS14
? track.artistTitle()
: track.artistName(),
spotifyTrackId: track.URI().spt_trackIdentifier()
@@ -185,20 +46,17 @@ private func loadLyricsForCurrentTrack() throws {
case .notReplaced:
throw LyricsError.invalidSource
}
let lyricsDto: LyricsDto
//
lastLyricsState = LyricsLoadingState()
lyricsState = LyricsLoadingState()
do {
lyricsDto = try repository.getLyrics(searchQuery, options: options)
}
catch let error {
if let error = error as? LyricsError {
lastLyricsState.fallbackError = error
lyricsState.fallbackError = error
switch error {
@@ -229,50 +87,39 @@ private func loadLyricsForCurrentTrack() throws {
}
}
else {
lastLyricsState.fallbackError = .unknownError
lyricsState.fallbackError = .unknownError
}
if source == .genius || !UserDefaults.lyricsOptions.geniusFallback {
throw error
}
NSLog("[EeveeSpotify] Unable to load lyrics from \(source): \(error), trying Genius as fallback")
source = .genius
repository = GeniusLyricsRepository()
lyricsDto = try repository.getLyrics(searchQuery, options: options)
}
lastLyricsState.areEmpty = lyricsDto.lines.isEmpty
lyricsState.isEmpty = lyricsDto.lines.isEmpty
lastLyricsState.wasRomanized = lyricsDto.romanization == .romanized
lyricsState.wasRomanized = lyricsDto.romanization == .romanized
|| (lyricsDto.romanization == .canBeRomanized && UserDefaults.lyricsOptions.romanization)
lastLyricsState.loadedSuccessfully = true
lyricsState.loadedSuccessfully = true
let lyrics = Lyrics.with {
$0.data = lyricsDto.toLyricsData(source: source.description)
$0.data = lyricsDto.toSpotifyLyricsData(source: source.description)
}
preloadedLyrics = lyrics
return lyrics
}
func getLyricsForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data {
guard let track = HookedInstances.currentTrack else {
func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data {
guard let track = nowPlayingScrollViewController?.loadedTrack else {
throw LyricsError.noCurrentTrack
}
var lyrics = preloadedLyrics
if lyrics == nil {
try loadLyricsForCurrentTrack()
lyrics = preloadedLyrics
}
guard var lyrics = lyrics else {
throw LyricsError.unknownError
}
var lyrics = try loadCustomLyricsForCurrentTrack()
let lyricsColorsSettings = UserDefaults.lyricsColors
@@ -280,30 +127,36 @@ func getLyricsForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data {
lyrics.colors = originalLyrics.colors
}
else {
var color: Color?
let extractedColor = switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14:
track.extractedColorHex()
default:
track.metadata()["extracted_color"]
}
let extractedColor = EeveeSpotify.isOldSpotifyVersion
? track.extractedColorHex()
: track.metadata()["extracted_color"]
var color: Color
if let extractedColor = extractedColor {
if lyricsColorsSettings.useStaticColor {
color = Color(hex: lyricsColorsSettings.staticColor)
}
else if let extractedColor = extractedColor {
color = Color(hex: extractedColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}
else if let uiColor = HookedInstances.nowPlayingMetaBackgroundModel?.color() {
else if let uiColor = nowPlayingScrollViewController?.backgroundViewController.color() {
color = Color(uiColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}
else {
color = Color.gray
}
color = color?.normalized(lyricsColorsSettings.normalizationFactor)
lyrics.colors = LyricsColors.with {
$0.backgroundColor = lyricsColorsSettings.useStaticColor
? Color(hex: lyricsColorsSettings.staticColor).uInt32
: color?.uInt32 ?? Color.gray.uInt32
$0.backgroundColor = color.uInt32
$0.lineColor = Color.black.uInt32
$0.activeLineColor = Color.white.uInt32
}
}
preloadedLyrics = nil
return try lyrics.serializedBytes()
}
@@ -0,0 +1,32 @@
import Orion
extension NowPlayingScrollViewController {
private var nowPlayingScrollViewModel: NSObject {
get {
Ivars<NSObject>(self).scrollViewModel
}
}
var scrollEnabled: Bool {
get {
Ivars<Bool>(nowPlayingScrollViewModel).scrollEnabled
}
set {
Ivars<Bool>(nowPlayingScrollViewModel).scrollEnabled = newValue
}
}
var loadedTrack: SPTPlayerTrack {
get {
Ivars<SPTPlayerTrack>(nowPlayingScrollViewModel).loadedTrack
}
}
//
var backgroundViewController: SPTNowPlayingBackgroundViewController {
get {
Ivars<SPTNowPlayingBackgroundViewController>(self).backgroundViewController
}
}
}
@@ -0,0 +1,5 @@
import UIKit
@objc protocol SPTNowPlayingBackgroundViewController {
func color() -> UIColor
}
@@ -6,7 +6,7 @@ struct LyricsDto {
var romanization: LyricsRomanizationStatus
var translation: LyricsTranslationDto?
func toLyricsData(source: String) -> LyricsData {
func toSpotifyLyricsData(source: String) -> LyricsData {
var lyricsData = LyricsData.with {
$0.timeSynchronized = timeSynced
$0.restriction = .unrestricted
@@ -2,7 +2,7 @@ import Foundation
struct LyricsLoadingState {
var wasRomanized = false
var areEmpty = false
var isEmpty = false
var fallbackError: LyricsError? = nil
var loadedSuccessfully = false
}
@@ -0,0 +1,23 @@
import Orion
import UIKit
var nowPlayingScrollViewController: NowPlayingScrollViewController?
class NowPlayingScrollViewControllerInstanceHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static let targetName = "NowPlaying_ScrollImpl.NowPlayingScrollViewController"
func nowPlayingScrollViewModelWithDidMoveToRelativeTrack(
_ track: SPTPlayerTrack,
withDifferentProviders: Bool,
scrollEnabledValueChanged: Bool
) -> NowPlayingScrollViewController {
nowPlayingScrollViewController = orig.nowPlayingScrollViewModelWithDidMoveToRelativeTrack(
track,
withDifferentProviders: withDifferentProviders,
scrollEnabledValueChanged: scrollEnabledValueChanged
)
return nowPlayingScrollViewController!
}
}
@@ -1,5 +0,0 @@
import Foundation
@objc protocol SPTCoreProductState {
func stringForKey(_ key: String) -> String
}
@@ -1,5 +0,0 @@
import UIKit
@objc protocol SPTNowPlayingMetadataBackgroundViewModel {
func color() -> UIColor
}
@@ -1,11 +0,0 @@
import Foundation
@objc protocol SPTPlayerTrack {
func setMetadata(_ metadata: [String:String])
func metadata() -> [String:String]
func extractedColorHex() -> String?
func trackTitle() -> String
func artistTitle() -> String
func artistName() -> String
func URI() -> SPTURL
}
@@ -10,9 +10,10 @@ private func showHavePremiumPopUp() {
class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDelegate {
static var targetName: String {
EeveeSpotify.isOldSpotifyVersion
? "SPTCoreURLSessionDataDelegate"
: "SPTDataLoaderService"
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "SPTCoreURLSessionDataDelegate"
default: return "SPTDataLoaderService"
}
}
func URLSession(
@@ -2,6 +2,8 @@ import Orion
import Intents
class INMediaItemHook: ClassHook<INMediaItem> {
typealias Group = PremiumPatchingGroup
func identifier() -> String {
var identifier = orig.identifier()
@@ -9,7 +9,7 @@ class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook<NSObject> {
onDemandSet: Any,
onDemandTrialService: Any,
trackRowsEnabled: Bool,
productState: SPTCoreProductState
productState: NSObject
) -> Target {
return orig.initWithViewURI(
uri,
@@ -0,0 +1,11 @@
import UIKit
import Orion
struct HookTargetNameHelper {
static var lyricsScrollProvider: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "Lyrics_CoreImpl.ScrollProvider"
default: return "Lyrics_NPVCommunicatorImpl.ScrollProvider"
}
}
}
@@ -2,7 +2,6 @@ import SwiftUI
import UIKit
extension UIColor {
func mix(with target: UIColor, amount: CGFloat) -> Self {
var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0
var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0
@@ -0,0 +1,5 @@
enum VersionHookTarget {
case latest
case lastAvailableiOS15
case lastAvailableiOS14
}
+13 -2
View File
@@ -12,8 +12,19 @@ struct PremiumPatchingGroup: HookGroup { }
struct EeveeSpotify: Tweak {
static let version = "5.9.9"
static let isOldSpotifyVersion =
NSClassFromString("Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController") == nil
static var hookTarget: VersionHookTarget {
let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
switch version {
case "9.0.48":
return .lastAvailableiOS15
case "8.9.8":
return .lastAvailableiOS14
default:
return .latest
}
}
init() {
if UserDefaults.darkPopUps {