Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c33061d
Update DataLoaderServiceHooks.x.swift to improve visual ads ad-blocking
jaydenjcpy Apr 7, 2026
bc5df6b
Merge branch 'Meeep1:main' into patch-3
jaydenjcpy Apr 15, 2026
85c0df5
Create main.yml
jaydenjcpy Apr 15, 2026
c52a886
Update SessionProtection.x.swift
jaydenjcpy Apr 18, 2026
3a6d987
Update Tweak.x.swift
jaydenjcpy Apr 18, 2026
bd1bcea
Update Tweak.x.swift
jaydenjcpy Apr 18, 2026
b23d9ef
Update Tweak.x.swift
jaydenjcpy Apr 18, 2026
288073f
Update Tweak.x.swift
jaydenjcpy Apr 18, 2026
34c8edd
Update Tweak.x.swift
jaydenjcpy Apr 18, 2026
aa1c83e
Update Tweak.x.swift
jaydenjcpy Apr 19, 2026
09d3585
Update EeveeSettingsUniversal.x.swift
jaydenjcpy Apr 19, 2026
26fa933
Update main.yml
jaydenjcpy Apr 19, 2026
4f3ef30
Update DynamicPremium+ModifyingFunctions.swift
jaydenjcpy Apr 19, 2026
fffaee4
Update UserDefaults+Extension.swift
jaydenjcpy Apr 19, 2026
51740c9
Update DynamicPremium+ModifyBootstrap.x.swift
jaydenjcpy Apr 19, 2026
5685c03
Update LikedSongsEnabler.x.swift
jaydenjcpy Apr 20, 2026
1168487
Update SiriNoPlayAsRadio.x.swift
jaydenjcpy Apr 20, 2026
322c5fd
Update DataLoaderServiceHooks.x.swift
jaydenjcpy Apr 20, 2026
80f888f
Update Tweak.x.swift
jaydenjcpy Apr 20, 2026
bfd32aa
Update DynamicPremium+ModifyBootstrap.x.swift
jaydenjcpy Apr 20, 2026
7c4d266
Update DataLoaderServiceHooks.x.swift
jaydenjcpy Apr 20, 2026
0c87d4e
Update UserDefaults+Extension.swift
jaydenjcpy Apr 20, 2026
41f92bc
Update DynamicPremium+ModifyBootstrap.x.swift
jaydenjcpy Apr 20, 2026
957cd8e
Update DataLoaderServiceHooks.x.swift
jaydenjcpy Apr 20, 2026
14fcda2
Update SessionProtection.x.swift
jaydenjcpy Apr 20, 2026
c8aa347
Update Tweak.x.swift
jaydenjcpy Apr 20, 2026
134f59e
Update SessionProtection.x.swift
jaydenjcpy Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Build Rootless Deb and inject .deb into IPA

on:
workflow_dispatch:
inputs:
branch:
description: "Branch to build from"
default: "main"
required: true
type: string
ipa_url:
description: "URL to the decrypted IPA file (optional for IPA build)"
default: ""
required: false
type: string

env:
THEOS: ${{ github.workspace }}/theos
SWIFTPROTOBUF_VERSION: "1.29.0"

jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
fetch-depth: 0

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2.0'

- name: Setup Theos
uses: actions/checkout@v4
with:
repository: theos/theos
path: ${{ github.workspace }}/theos
submodules: recursive

- name: Install Dependencies
run: |
brew install make dpkg ldid wget
# Ensure we use GNU Make
echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH
- name: Setup SwiftProtobuf (Rootless)
run: |
mkdir -p swiftprotobuf-rootless && cd swiftprotobuf-rootless
wget https://github.com/whoeevee/swift-protobuf/releases/download/${{ env.SWIFTPROTOBUF_VERSION }}/org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm64.deb
ar -x org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm64.deb
tar -xf data.tar.lzma
mkdir -p $THEOS/lib/iphone/rootless
cp -r var/jb/Library/Frameworks/SwiftProtobuf.framework $THEOS/lib/iphone/rootless/
- name: Build Rootless Package
run: |
make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless
# Find the generated deb file
DEB_PATH=$(find packages -name "*.deb" | head -n 1)
echo "DEB_PATH=$DEB_PATH" >> $GITHUB_ENV
echo "DEB_NAME=$(basename $DEB_PATH)" >> $GITHUB_ENV
- name: Upload DEB Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.DEB_NAME }}
path: ${{ env.DEB_PATH }}
if-no-files-found: error

- name: Build IPA (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
# Download and Validate IPA
wget "${{ inputs.ipa_url }}" --no-verbose -O ${{ github.workspace }}/spotify.ipa
file_type=$(file --mime-type -b ${{ github.workspace }}/spotify.ipa)
if [[ "$file_type" != "application/x-ios-app" && "$file_type" != "application/zip" ]]; then
echo "::error::Validation failed: The downloaded file is not a valid IPA. Detected type: $file_type."
exit 1
fi
- name: Download SwiftProtobuf for IPA (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
wget https://github.com/whoeevee/swift-protobuf/releases/download/${{ env.SWIFTPROTOBUF_VERSION }}/org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm.deb
wget https://github.com/whoeevee/swift-protobuf/releases/download/${{ env.SWIFTPROTOBUF_VERSION }}/org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm64.deb
echo "SWIFTPROTOBUF_ROOTFUL=org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm.deb" >> $GITHUB_ENV
echo "SWIFTPROTOBUF_ROOTLESS=org.swift.protobuf.swiftprotobuf_${{ env.SWIFTPROTOBUF_VERSION }}_iphoneos-arm64.deb" >> $GITHUB_ENV
- name: Download OpenSpotifySafariExtension (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
git clone https://github.com/BillyCurtis/OpenSpotifySafariExtension
echo "OPENSPOTIFYSAFARIEXTENSION=OpenSpotifySafariExtension/OpenSpotifySafariExtension.appex" >> $GITHUB_ENV
- name: Setup insert-dylib (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
git clone https://github.com/Tyilo/insert_dylib.git
xcrun clang -x c -arch arm64 ./insert_dylib/insert_dylib/main.c -I/usr/include/ -o insert-dylib
sudo mv insert-dylib /usr/local/bin/
sudo chmod +x /usr/local/bin/insert-dylib
- name: Setup ivinject (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
git clone https://github.com/whoeevee/ivinject.git
cp -r ./ivinject/KnownFrameworks ~/.ivinject
wget https://github.com/whoeevee/ivinject/releases/download/first/ivinject-arm64
sudo mv ivinject-arm64 /usr/local/bin/
sudo chmod +x /usr/local/bin/ivinject-arm64
- name: Remove Watch App from IPA (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
unzip -q spotify.ipa -d spotify_temp
rm -rf spotify_temp/Payload/Spotify.app/Watch
rm -rf spotify_temp/Payload/Spotify.app/PlugIns/*.appex
cd spotify_temp
zip -qr ../spotify_clean.ipa Payload
cd ..
rm -rf spotify_temp
mv spotify_clean.ipa spotify.ipa
- name: Create EeveeSpotify IPA Package (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
run: |
ivinject-arm64 spotify.ipa EeveeSpotify.ipa \
-i ${{ env.DEB_PATH }} "$OPENSPOTIFYSAFARIEXTENSION" \
-s - -d --level Optimal
- name: Upload IPA Artifact (if IPA URL provided)
if: ${{ inputs.ipa_url != '' }}
uses: actions/upload-artifact@v4
with:
name: EeveeSpotify.ipa
path: ${{ github.workspace }}/EeveeSpotify.ipa
if-no-files-found: warn
20 changes: 18 additions & 2 deletions Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ func DataLoaderServiceHooks_startCapturing() {
}

class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
// Intercepts various responses (customize/plan/lyrics) and now also bootstrap for 9.1.x stability.
typealias Group = PremiumBootstrapGroup
static let targetName = "SPTDataLoaderService"

// orion:new
Expand Down Expand Up @@ -59,15 +61,15 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {

// orion:new
func shouldModify(_ url: URL) -> Bool {
let shouldPatchPremium = BasePremiumPatchingGroup.isActive
let shouldPatchPremium = BasePremiumPatchingGroup.isActive || PremiumBootstrapGroup.isActive
let shouldReplaceLyrics = BaseLyricsGroup.isActive

let isLyricsURL = url.isLyrics
let path = url.path.lowercased()
let isDAC = path.contains("/dac/view/v1/")

return (shouldReplaceLyrics && isLyricsURL)
|| (shouldPatchPremium && (url.isCustomize || url.isPremiumPlanRow || url.isPremiumBadge || url.isPlanOverview || isDAC))
|| (shouldPatchPremium && (url.isBootstrap || url.isCustomize || url.isPremiumPlanRow || url.isPremiumBadge || url.isPlanOverview || isDAC))
}

// orion:new
Expand Down Expand Up @@ -218,6 +220,20 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
return
}

if url.isBootstrap {
// Patch bootstrap on the SPTDataLoaderService path too.
// Some builds / sessions do not hit SpotifySessionDelegateBootstrapHook reliably.
var bootstrapMessage = try BootstrapMessage(serializedBytes: buffer)
writeDebugLog("[BOOTSTRAP] (DL) Patching bootstrap UCS response")
UserDefaults.hasPatchedBootstrap = true
if UserDefaults.patchType == .requests {
modifyRemoteConfiguration(&bootstrapMessage.ucsResponse)
}
respondWithCustomData(try bootstrapMessage.serializedBytes(), task: task, session: session)
orig.URLSession(session, task: task, didCompleteWithError: nil)
return
}

if url.isCustomize {
var customizeMessage = try CustomizeMessage(serializedBytes: buffer)
modifyRemoteConfiguration(&customizeMessage.response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ private func showHavePremiumPopUp() {
}

class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDelegate {
// This hook is the *core* of premium patching (intercepts bootstrap and mutates UCS).
typealias Group = PremiumBootstrapGroup
static var targetName: String {
switch EeveeSpotify.hookTarget {
case .lastAvailableiOS14: return "SPTCoreURLSessionDataDelegate"
Expand Down Expand Up @@ -82,6 +84,8 @@ class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDe
}

if UserDefaults.patchType == .requests {
writeDebugLog("[BOOTSTRAP] Patching bootstrap UCS response")
UserDefaults.hasPatchedBootstrap = true
modifyRemoteConfiguration(&bootstrapMessage.ucsResponse)

orig.URLSession(
Expand All @@ -91,6 +95,7 @@ class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDe
)
}
else {
writeDebugLog("[BOOTSTRAP] Passing through unmodified bootstrap (patchType=\(UserDefaults.patchType))")
orig.URLSession(session, dataTask: task, didReceiveData: buffer)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import UIKit
func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
modifyAttributes(&configuration.attributes.accountAttributes)

// IMPORTANT:
// Always apply our assignedValues patching. Some accounts receive different remote configs,
// and using a static bundled resolve config alone can regress back to Free tier.
modifyAssignedValues(&configuration.assignedValues)

if UserDefaults.overwriteConfiguration {
configuration.resolve.configuration = try! BundleHelper.shared.resolveConfiguration()
}
else {
modifyAssignedValues(&configuration.assignedValues)
}
}

private let propertyReplacements = [
Expand Down
2 changes: 1 addition & 1 deletion Sources/EeveeSpotify/Premium/LikedSongsEnabler.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ private let likedTracksRow: [String: Any] = [
]

class HUBViewModelBuilderImplementationHook: ClassHook<NSObject> {
typealias Group = BasePremiumPatchingGroup
typealias Group = PremiumUIHooksGroup
static let targetName: String = "HUBViewModelBuilderImplementation"

func addJSONDictionary(_ dictionary: NSDictionary?) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/EeveeSpotify/Premium/SiriNoPlayAsRadio.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Orion
import Intents

class INMediaItemHook: ClassHook<INMediaItem> {
typealias Group = BasePremiumPatchingGroup
typealias Group = PremiumUIHooksGroup

func identifier() -> String {
var identifier = orig.identifier()
Expand Down
33 changes: 25 additions & 8 deletions Sources/EeveeSpotify/SessionProtection.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import Foundation
// Additionally blocks network endpoints that trigger session invalidation.
// Extends OAuth token expiry to prevent internal reauth triggers.

struct SessionLogoutHookGroup: HookGroup { }
// Split into smaller groups so missing selectors/classes don't crash on activation.
// Spotify occasionally renames/removes private session-related selectors between minor versions.
// By gating each group behind runtime checks, we keep compatibility across 9.1.x.
struct SessionLogoutAuthHookGroup: HookGroup { }
struct SessionLogoutConnectivityHookGroup: HookGroup { }
struct SessionLogoutAblyHookGroup: HookGroup { }
struct SessionLogoutNetworkHookGroup: HookGroup { }

// Ably action name mapping for readable logs
private let ablyActionNames: [Int: String] = [
Expand All @@ -21,7 +27,7 @@ private let ablyActionNames: [Int: String] = [
// MARK: - SPTAuthSessionImplementation — Core Session Hooks

class SPTAuthSessionHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutAuthHookGroup
static let targetName = "SPTAuthSessionImplementation"

// orion:new
Expand Down Expand Up @@ -91,7 +97,7 @@ class SPTAuthSessionHook: ClassHook<NSObject> {
// MARK: - SessionServiceImpl (Connectivity_SessionImpl module)

class SessionServiceImplHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutConnectivityHookGroup
static let targetName = "_TtC24Connectivity_SessionImpl18SessionServiceImpl"

func automatedLogoutThenLogin() {
Expand Down Expand Up @@ -126,7 +132,7 @@ class SessionServiceImplHook: ClassHook<NSObject> {
// MARK: - SPTAuthLegacyLoginControllerImplementation

class LegacyLoginControllerHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutAuthHookGroup
static let targetName = "SPTAuthLegacyLoginControllerImplementation"

func sessionDidLogout(_ session: AnyObject, withReason reason: AnyObject) {
Expand Down Expand Up @@ -172,7 +178,7 @@ class LegacyLoginControllerHook: ClassHook<NSObject> {
// the internal timer from marking the token as expired.

class OauthAccessTokenBridgeHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutConnectivityHookGroup
static let targetName = "_TtC24Connectivity_SessionImplP33_831B98CC28223E431E21CD27ADD20AF222OauthAccessTokenBridge"

// Hook the GETTER
Expand Down Expand Up @@ -245,7 +251,7 @@ private func extractAblyAction(_ text: String) -> Int? {
}

class ARTWebSocketTransportHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutAblyHookGroup
static let targetName = "ARTWebSocketTransport"

func webSocket(_ ws: AnyObject, didReceiveMessage message: AnyObject) {
Expand Down Expand Up @@ -286,7 +292,7 @@ class ARTWebSocketTransportHook: ClassHook<NSObject> {
// MARK: - Ably SRWebSocket Frame Hook

class ARTSRWebSocketHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutAblyHookGroup
static let targetName = "ARTSRWebSocket"

func _handleFrameWithData(_ data: NSData, opCode code: Int) {
Expand Down Expand Up @@ -321,7 +327,7 @@ class ARTSRWebSocketHook: ClassHook<NSObject> {
// MARK: - Global URLSessionTask hook to catch auth traffic bypassing SPTDataLoaderService

class URLSessionTaskResumeHook: ClassHook<NSObject> {
typealias Group = SessionLogoutHookGroup
typealias Group = SessionLogoutNetworkHookGroup
static let targetName = "NSURLSessionTask"

func resume() {
Expand All @@ -333,6 +339,17 @@ class URLSessionTaskResumeHook: ClassHook<NSObject> {
let elapsedInt = Int(elapsed)
let path = url.path

// If we've already patched bootstrap once in this session, block any subsequent
// bootstrap calls. Some 9.1.x builds perform a second bootstrap very early during
// an internal session re-init, and that second response can overwrite our premium state.
// Only cancel *subsequent* bootstrap calls in the same launch cycle.
// Never cancel the initial bootstrap during startup, or Spotify can hang on splash.
if path.contains("bootstrap/v1/bootstrap"), UserDefaults.hasPatchedBootstrap, elapsed > 5 {
writeDebugLog("[NET] Cancelled bootstrap re-fetch at \(elapsedInt)s")
task.cancel()
return
}

// Log auth-related requests for diagnostics
let isAuthRelated = host.contains("login5") ||
host.contains("apresolve") ||
Expand Down
17 changes: 11 additions & 6 deletions Sources/EeveeSpotify/Settings/EeveeSettingsUniversal.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import Orion
import SwiftUI
import UIKit

// Universal settings integration that works across all Spotify versions
struct UniversalSettingsIntegrationGroup: HookGroup { }
// Universal settings integration.
// Split into multiple HookGroups so missing classes in newer Spotify builds
// (e.g., RootSettingsViewController removed in 9.1.36) don't crash when activating.
struct UniversalSettingsIntegrationProfileGroup: HookGroup { }
struct UniversalSettingsIntegrationSettingsVCGroup: HookGroup { }
struct UniversalSettingsIntegrationRootSettingsVCGroup: HookGroup { }
struct UniversalSettingsIntegrationNavGroup: HookGroup { }

// MARK: - Primary: ProfileSettingsSection hook for settings menu row
class UniversalProfileSettingsSectionHook: ClassHook<NSObject> {
typealias Group = UniversalSettingsIntegrationGroup
typealias Group = UniversalSettingsIntegrationProfileGroup
static let targetName = "ProfileSettingsSection"

func numberOfRows() -> Int {
Expand Down Expand Up @@ -169,7 +174,7 @@ func injectEeveeButton(into target: UIViewController) {

// MARK: - Fallback: Hook SettingsViewController directly (New UI)
class SettingsViewControllerHook: ClassHook<UIViewController> {
typealias Group = UniversalSettingsIntegrationGroup
typealias Group = UniversalSettingsIntegrationSettingsVCGroup
static let targetName = "SettingsViewController"

func viewDidLoad() {
Expand All @@ -185,7 +190,7 @@ class SettingsViewControllerHook: ClassHook<UIViewController> {

// MARK: - Fallback: Hook RootSettingsViewController directly
class RootSettingsViewControllerHook: ClassHook<UIViewController> {
typealias Group = UniversalSettingsIntegrationGroup
typealias Group = UniversalSettingsIntegrationRootSettingsVCGroup
static let targetName = "RootSettingsViewController"

func viewDidLoad() {
Expand All @@ -201,7 +206,7 @@ class RootSettingsViewControllerHook: ClassHook<UIViewController> {

// MARK: - Generic Fallback: Hook UINavigationController to catch Settings by title/class name
class SettingsNavigationStackHook: ClassHook<UINavigationController> {
typealias Group = UniversalSettingsIntegrationGroup
typealias Group = UniversalSettingsIntegrationNavGroup

func pushViewController(_ viewController: UIViewController, animated: Bool) {
orig.pushViewController(viewController, animated: animated)
Expand Down
Loading