fixes and improvements

This commit is contained in:
eevee
2025-11-02 01:54:38 +03:00
parent f013a597c6
commit f39d1aead3
21 changed files with 267 additions and 137 deletions

View File

@@ -6,8 +6,8 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
// orion:new
func shouldModify(_ url: URL) -> Bool {
let shouldPatchPremium = PremiumPatchingGroup.isActive
let shouldReplaceLyrics = LyricsGroup.isActive
let shouldPatchPremium = BasePremiumPatchingGroup.isActive
let shouldReplaceLyrics = BaseLyricsGroup.isActive
return (shouldReplaceLyrics && url.isLyrics)
|| (shouldPatchPremium && (url.isCustomize || url.isPremiumPlanRow || url.isPremiumBadge || url.isPlanOverview))
@@ -33,61 +33,53 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
return
}
guard let buffer = URLSessionHelper.shared.obtainData(for: url) else {
return
}
do {
if let buffer = URLSessionHelper.shared.obtainData(for: url) {
if url.isLyrics {
respondWithCustomData(
try getLyricsDataForCurrentTrack(
originalLyrics: try? Lyrics(serializedBytes: buffer)
),
task: task,
session: session
)
return
}
if url.isPremiumPlanRow {
respondWithCustomData(
try getPremiumPlanRowData(
originalPremiumPlanRow: try PremiumPlanRow(serializedBytes: buffer)
),
task: task,
session: session
)
return
}
if url.isPremiumBadge {
respondWithCustomData(try getPremiumPlanBadge(), task: task, session: session)
return
}
if url.isLyrics {
respondWithCustomData(
try getLyricsDataForCurrentTrack(
originalLyrics: try? Lyrics(serializedBytes: buffer)
),
task: task,
session: session
)
return
}
if url.isPremiumPlanRow {
respondWithCustomData(
try getPremiumPlanRowData(
originalPremiumPlanRow: try PremiumPlanRow(serializedBytes: buffer)
),
task: task,
session: session
)
return
}
if url.isPremiumBadge {
respondWithCustomData(try getPremiumPlanBadge(), task: task, session: session)
return
}
if url.isCustomize {
var customizeMessage = try CustomizeMessage(serializedBytes: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
respondWithCustomData(try customizeMessage.serializedData(), task: task, session: session)
return
}
if url.isPlanOverview {
do {
orig.URLSession(session, dataTask: task, didReceiveData: try getPlanOverviewData())
orig.URLSession(session, task: task, didCompleteWithError: nil)
}
catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
respondWithCustomData(try getPlanOverviewData(), task: task, session: session)
return
}
}
catch {
orig.URLSession(session, task: task, didCompleteWithError: error)
}
orig.URLSession(session, task: task, didCompleteWithError: error)
}
func URLSession(

View File

@@ -1,22 +1,35 @@
import Orion
import UIKit
private var shouldOverrideLocalTrackURI = false
class SPTPlayerTrackHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static let targetName = EeveeSpotify.hookTarget == .latest
? "SPTPlayerTrackImplementation"
: "SPTPlayerTrack"
func metadata() -> [String: String] {
var meta = orig.metadata()
meta["has_lyrics"] = "true"
return meta
}
func URI() -> NSURL? {
let uri = orig.URI()
guard shouldOverrideLocalTrackURI,
let absoluteString = uri?.absoluteString,
absoluteString.hasPrefix("spotify:local:") else {
return uri
}
return NSURL(string: "spotify:track:")!
}
}
class LyricsScrollProviderHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static var targetName = HookTargetNameHelper.lyricsScrollProvider
func isEnabledForTrack(_ track: SPTPlayerTrack) -> Bool {
@@ -24,8 +37,23 @@ class LyricsScrollProviderHook: ClassHook<NSObject> {
}
}
class NPVScrollViewControllerHook: ClassHook<NSObject> {
typealias Group = ModernLyricsGroup
static var targetName = "NowPlaying_ScrollImpl.NPVScrollViewController"
func viewWillAppear(_ animated: Bool) {
shouldOverrideLocalTrackURI = true
orig.viewWillAppear(animated)
}
func viewWillDisappear(_ animated: Bool) {
shouldOverrideLocalTrackURI = false
orig.viewWillDisappear(animated)
}
}
class NowPlayingScrollViewControllerHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
typealias Group = LegacyLyricsGroup
static var targetName = "NowPlaying_ScrollImpl.NowPlayingScrollViewController"
func nowPlayingScrollViewModelWithDidLoadComponentsFor(

View File

@@ -2,12 +2,13 @@ import Orion
import UIKit
class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "Lyrics_CoreImpl.FullscreenViewController"
default: return "Lyrics_FullscreenPageImpl.FullscreenViewController"
case .lastAvailableiOS15: return "Lyrics_FullscreenPageImpl.FullscreenViewController"
default: return "Lyrics_FullscreenElementPageImpl.FullscreenElementViewController"
}
}
@@ -21,6 +22,26 @@ class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {
return
}
if EeveeSpotify.hookTarget == .latest {
guard let fullscreenView = WindowHelper.shared.findFirstSubview(
"Lyrics_FullscreenElementPageImpl.FullscreenView",
in: target.view
) else {
return
}
let controlsView = Ivars<UIView>(fullscreenView).controlsView
let contextMenuButtonContainer = Ivars<UIView>(controlsView).contextMenuButtonContainer
if let contextButton = contextMenuButtonContainer.subviews(
matching: "Encore6Button"
).first as? UIControl {
contextButton.isEnabled = false
}
return
}
let headerView = Ivars<UIView>(target.view).headerView
if let reportButton = headerView.subviews(matching: "EncoreButton")[1] as? UIButton {

View File

@@ -2,7 +2,7 @@ import Orion
import UIKit
class ErrorViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {
@@ -14,17 +14,29 @@ class ErrorViewControllerHook: ClassHook<UIViewController> {
func loadView() {
orig.loadView()
guard UserDefaults.lyricsOptions.hideOnError, let controller = nowPlayingScrollViewController else {
guard UserDefaults.lyricsOptions.hideOnError else {
return
}
var providers = controller.activeProviders
providers.removeAll(
where: { NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider }
)
controller.activeProviders = providers
controller.collectionView().reloadData()
if let controller = nowPlayingScrollViewController {
controller.dataSource.activeProviders.removeAll {
NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider
}
controller.collectionView().reloadData()
}
else if let controller = npvScrollViewController, let dataSource = scrollDataSource {
let lyricsProviderIndex = dataSource.activeProviders.firstIndex {
NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider
}
let collectionView = controller.collectionView()
let dataSource = Ivars<__UIDiffableDataSource>(collectionView.dataSource!)._impl
let itemIdentifiers = dataSource.itemIdentifiers()
let lyricsProviderItemIdentifier = itemIdentifiers[lyricsProviderIndex!]
dataSource.deleteItemsWithIdentifiers([lyricsProviderItemIdentifier])
}
}
}

View File

@@ -0,0 +1,22 @@
import UIKit
import Orion
class UITableViewHook: ClassHook<UITableView> {
typealias Group = BaseLyricsGroup
func scrollToRowAtIndexPath(
_ indexPath: NSIndexPath,
atScrollPosition scrollPosition: UITableView.ScrollPosition,
animated: Bool
) {
if target.numberOfRows(inSection: indexPath.section) == 0 {
return
}
orig.scrollToRowAtIndexPath(
indexPath,
atScrollPosition: scrollPosition,
animated: animated
)
}
}

View File

@@ -2,7 +2,7 @@ import Orion
import UIKit
class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {

View File

@@ -3,7 +3,10 @@ import SwiftUI
//
struct LyricsGroup: HookGroup { }
struct BaseLyricsGroup: HookGroup { }
struct LegacyLyricsGroup: HookGroup { }
struct ModernLyricsGroup: HookGroup { }
var lyricsState = LyricsLoadingState()
@@ -16,9 +19,12 @@ private let petitLyricsRepository = PetitLyricsRepository()
//
private func loadCustomLyricsForCurrentTrack() throws -> Lyrics {
guard let track = nowPlayingScrollViewController?.loadedTrack else {
throw LyricsError.noCurrentTrack
}
guard
let track = statefulPlayer?.currentTrack() ??
nowPlayingScrollViewController?.loadedTrack
else {
throw LyricsError.noCurrentTrack
}
let searchQuery = LyricsSearchQuery(
title: track.trackTitle(),
@@ -115,9 +121,12 @@ private func loadCustomLyricsForCurrentTrack() throws -> Lyrics {
}
func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data {
guard let track = nowPlayingScrollViewController?.loadedTrack else {
throw LyricsError.noCurrentTrack
}
guard
let track = statefulPlayer?.currentTrack() ??
nowPlayingScrollViewController?.loadedTrack
else {
throw LyricsError.noCurrentTrack
}
var lyrics = try loadCustomLyricsForCurrentTrack()
@@ -143,7 +152,7 @@ func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data
color = Color(hex: extractedColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}
else if let uiColor = nowPlayingScrollViewController?.backgroundViewModel.color() {
else if let uiColor = backgroundViewModel?.color() {
color = Color(uiColor)
.normalized(lyricsColorsSettings.normalizationFactor)
}

View File

@@ -0,0 +1,12 @@
import Orion
extension NowPlayingScrollDataSourceImplementation {
var activeProviders: Array<NSObject> {
get {
Ivars<Array<NSObject>>(self).activeProviders
}
set {
Ivars<Array<NSObject>>(self).activeProviders = newValue
}
}
}

View File

@@ -22,37 +22,9 @@ extension NowPlayingScrollViewController {
}
}
//
private var dataSource: NSObject {
var dataSource: NowPlayingScrollDataSourceImplementation {
get {
Ivars<NSObject>(nowPlayingScrollViewModel).dataSource
}
}
var activeProviders: Array<NSObject> {
get {
Ivars<Array<NSObject>>(dataSource).activeProviders
}
set {
Ivars<Array<NSObject>>(dataSource).activeProviders = newValue
}
}
//
private var backgroundViewController: NSObject {
get {
Ivars<NSObject>(self).backgroundViewController
}
}
var backgroundViewModel: SPTNowPlayingBackgroundViewModel {
get {
let ivars = Ivars<SPTNowPlayingBackgroundViewModel>(self.backgroundViewController)
return EeveeSpotify.hookTarget == .latest
? ivars.artworkColorObservable
: ivars.viewModel
Ivars<NowPlayingScrollDataSourceImplementation>(nowPlayingScrollViewModel).dataSource
}
}
}

View File

@@ -0,0 +1,6 @@
import Foundation
import UIKit
@objc protocol NPVScrollViewController {
func collectionView() -> UICollectionView
}

View File

@@ -0,0 +1,3 @@
import Foundation
@objc protocol NowPlayingScrollDataSourceImplementation { }

View File

@@ -0,0 +1,5 @@
import Foundation
@objc protocol StatefulPlayerImplementation {
func currentTrack() -> SPTPlayerTrack?
}

View File

@@ -0,0 +1,6 @@
import UIKit
@objc protocol __UIDiffableDataSource {
func itemIdentifiers() -> NSArray
func deleteItemsWithIdentifiers(_ identifiers: NSArray)
}

View File

@@ -1,39 +1,39 @@
import Orion
import UIKit
var nowPlayingScrollViewController: NowPlayingScrollViewController?
var statefulPlayer: StatefulPlayerImplementation?
var backgroundViewModel: SPTNowPlayingBackgroundViewModel?
var scrollDataSource: NowPlayingScrollDataSourceImplementation?
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!
}
}
var nowPlayingScrollViewController: NowPlayingScrollViewController?
var npvScrollViewController: NPVScrollViewController?
class NowPlayingScrollPrivateServiceImplementationHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
typealias Group = BaseLyricsGroup
static let targetName = "NowPlaying_ScrollImpl.NowPlayingScrollPrivateServiceImplementation"
func provideScrollViewControllerWithDependencies(_ dependencies: NSObject) -> UIViewController {
// spotify introduced some "nova scroll" with different controllers and logic
// hope they don't remove backward compatibility, i don't want to rewrite ts 😭🙏
let scrollViewController = orig.provideScrollViewControllerWithDependencies(dependencies)
if EeveeSpotify.hookTarget != .lastAvailableiOS14 {
Ivars<Bool>(target).$__lazy_storage_$_isNovaScrollEnabled = false
if NSStringFromClass(type(of: scrollViewController)) ~= "NowPlayingScrollViewController" {
nowPlayingScrollViewController = Dynamic.convert(
scrollViewController,
to: NowPlayingScrollViewController.self
)
}
else {
statefulPlayer = Ivars<StatefulPlayerImplementation>(dependencies).statefulPlayer
scrollDataSource = Ivars<NowPlayingScrollDataSourceImplementation>(target)
.$__lazy_storage_$_scrollDataSource
npvScrollViewController = Dynamic.convert(
scrollViewController,
to: NPVScrollViewController.self
)
}
return orig.provideScrollViewControllerWithDependencies(dependencies)
backgroundViewModel = Ivars<SPTNowPlayingBackgroundViewModel>(dependencies)
.backgroundViewModel
return scrollViewController
}
}

View File

@@ -70,7 +70,7 @@ class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDe
}
else {
UserDefaults.patchType = .requests
PremiumPatchingGroup().activate()
activatePremiumPatchingGroup()
}
NSLog("[EeveeSpotify] Fetched bootstrap, \(UserDefaults.patchType) was set")

View File

@@ -6,7 +6,7 @@ private let likedTracksRow: [String: Any] = [
]
class HUBViewModelBuilderImplementationHook: ClassHook<NSObject> {
typealias Group = PremiumPatchingGroup
typealias Group = BasePremiumPatchingGroup
static let targetName: String = "HUBViewModelBuilderImplementation"
func addJSONDictionary(_ dictionary: NSDictionary?) {

View File

@@ -1,22 +1,42 @@
import Orion
import UIKit
private func showHighQualityPopUp() {
PopUpHelper.showPopUp(
message: "high_audio_quality_popup".localized,
buttonText: "OK".uiKitLocalized
)
}
class ListRowInteractionListenerViewHook: ClassHook<UIView> {
typealias Group = ModernPremiumPatchingGroup
static let targetName = "_TtC15Settings_ECMKit30ListRowInteractionListenerView"
func performAction() {
guard
let accessibilityLabel = target.subviews.first?.accessibilityLabel,
accessibilityLabel.hasSuffix("Premium")
else {
orig.performAction()
return
}
showHighQualityPopUp()
}
}
class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
typealias Group = PremiumPatchingGroup
typealias Group = LegacyPremiumPatchingGroup
static let targetName = "StreamQualitySettingsSection"
func shouldResetSelection() -> Bool {
PopUpHelper.showPopUp(
message: "high_audio_quality_popup".localized,
buttonText: "OK".uiKitLocalized
)
showHighQualityPopUp()
return true
}
}
class ContentOffliningUIHelperImplementationHook: ClassHook<NSObject> {
typealias Group = PremiumPatchingGroup
typealias Group = BasePremiumPatchingGroup
static let targetName = "Offline_ContentOffliningUIImpl.ContentOffliningUIHelperImplementation"
func downloadToggledWithCurrentAvailability(

View File

@@ -2,7 +2,7 @@ import Orion
import Intents
class INMediaItemHook: ClassHook<INMediaItem> {
typealias Group = PremiumPatchingGroup
typealias Group = BasePremiumPatchingGroup
func identifier() -> String {
var identifier = orig.identifier()

View File

@@ -1,7 +1,7 @@
import Orion
class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook<NSObject> {
typealias Group = PremiumPatchingGroup
typealias Group = BasePremiumPatchingGroup
static let targetName = "SPTFreeTierArtistHubRemoteURLResolver"
func initWithViewURI(

View File

@@ -1,4 +1,5 @@
import Orion
import EeveeSpotifyC
import UIKit
func exitApplication() {
@@ -8,7 +9,21 @@ func exitApplication() {
}
}
struct PremiumPatchingGroup: HookGroup { }
struct BasePremiumPatchingGroup: HookGroup { }
struct LegacyPremiumPatchingGroup: HookGroup { }
struct ModernPremiumPatchingGroup: HookGroup { }
func activatePremiumPatchingGroup() {
BasePremiumPatchingGroup().activate()
if EeveeSpotify.hookTarget == .latest {
ModernPremiumPatchingGroup().activate()
}
else {
LegacyPremiumPatchingGroup().activate()
}
}
struct EeveeSpotify: Tweak {
static let version = "6.1.6"
@@ -36,11 +51,18 @@ struct EeveeSpotify: Tweak {
}
if UserDefaults.patchType.isPatching {
PremiumPatchingGroup().activate()
activatePremiumPatchingGroup()
}
if UserDefaults.lyricsSource.isReplacingLyrics {
LyricsGroup().activate()
BaseLyricsGroup().activate()
if EeveeSpotify.hookTarget == .latest {
ModernLyricsGroup().activate()
}
else {
LegacyLyricsGroup().activate()
}
}
}
}