actual fix for #437 and Do Not Replace Lyrics option

This commit is contained in:
eevee
2024-10-10 17:55:25 +03:00
parent a579a0709c
commit e0fe39d717
20 changed files with 248 additions and 188 deletions
@@ -6,8 +6,11 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
// orion:new
func shouldModify(_ url: URL) -> Bool {
let isModifyingCustomizeResponse = UserDefaults.patchType == .requests
return url.isLyrics || (url.isCustomize && isModifyingCustomizeResponse)
let isModifyingCustomizeResponse = PremiumPatchingGroup.isActive
let isModifyingLyrics = LyricsGroup.isActive
return (url.isLyrics && isModifyingLyrics)
|| (url.isCustomize && isModifyingCustomizeResponse)
}
func URLSession(
@@ -82,7 +85,7 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
return
}
if url.isLyrics, response.statusCode != 200 {
if shouldModify(url), url.isLyrics, response.statusCode != 200 {
let okResponse = HTTPURLResponse(
url: url,
statusCode: 200,
@@ -39,7 +39,6 @@ struct PopUpHelper {
dialog.update(model)
dialog.setEventHandler({ state in
switch (state) {
case .primary: onPrimaryClick?()
@@ -0,0 +1,27 @@
import Orion
class SPTPlayerTrackHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static let targetName = "SPTPlayerTrack"
func metadata() -> [String:String] {
var meta = orig.metadata()
meta["has_lyrics"] = "true"
return meta
}
}
class LyricsScrollProviderHook: ClassHook<NSObject> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.ScrollProvider"
: "Lyrics_NPVCommunicatorImpl.ScrollProvider"
}
func isEnabledForTrack(_ track: SPTPlayerTrack) -> Bool {
return true
}
}
@@ -1,32 +1,15 @@
import Orion
import SwiftUI
class SPTPlayerTrackHook: ClassHook<NSObject> {
static let targetName = "SPTPlayerTrack"
//
func metadata() -> [String:String] {
var meta = orig.metadata()
struct LyricsGroup: HookGroup { }
meta["has_lyrics"] = "true"
return meta
}
}
class LyricsScrollProviderHook: ClassHook<NSObject> {
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.ScrollProvider"
: "Lyrics_NPVCommunicatorImpl.ScrollProvider"
}
func isEnabledForTrack(_ track: SPTPlayerTrack) -> Bool {
return true
}
}
//
class LyricsFullscreenViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
? "Lyrics_CoreImpl.FullscreenViewController"
@@ -62,6 +45,7 @@ private var hasShownUnauthorizedPopUp = false
//
class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
typealias Group = LyricsGroup
static var targetName: String {
return EeveeSpotify.isOldSpotifyVersion
@@ -70,7 +54,6 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
}
func viewDidLoad() {
orig.viewDidLoad()
guard
@@ -162,6 +145,7 @@ private func loadLyricsForCurrentTrack() throws {
case .lrclib: LrcLibLyricsRepository()
case .musixmatch: MusixmatchLyricsRepository.shared
case .petit: PetitLyricsRepository()
case .notReplaced: throw LyricsError.InvalidSource
}
let lyricsDto: LyricsDto
@@ -180,7 +164,6 @@ private func loadLyricsForCurrentTrack() throws {
switch error {
case .InvalidMusixmatchToken:
if !hasShownUnauthorizedPopUp {
PopUpHelper.showPopUp(
delayed: false,
@@ -192,7 +175,6 @@ private func loadLyricsForCurrentTrack() throws {
}
case .MusixmatchRestricted:
if !hasShownRestrictedPopUp {
PopUpHelper.showPopUp(
delayed: false,
@@ -226,7 +208,7 @@ private func loadLyricsForCurrentTrack() throws {
lastLyricsState.areEmpty = lyricsDto.lines.isEmpty
lastLyricsState.wasRomanized = lyricsDto.romanization == .romanized
|| (lyricsDto.romanization == .canBeRomanized && UserDefaults.lyricsOptions.romanization)
|| (lyricsDto.romanization == .canBeRomanized && UserDefaults.lyricsOptions.romanization)
lastLyricsState.loadedSuccessfully = true
@@ -7,6 +7,7 @@ enum LyricsError: Error, CustomStringConvertible {
case DecodingError
case NoSuchSong
case UnknownError
case InvalidSource
var description: String {
switch self {
@@ -16,6 +17,7 @@ enum LyricsError: Error, CustomStringConvertible {
case .DecodingError: "decoding_error".localized
case .NoCurrentTrack: "no_current_track".localized
case .UnknownError: "unknown_error".localized
case .InvalidSource: ""
}
}
}
@@ -5,6 +5,11 @@ enum LyricsSource: Int, CaseIterable, CustomStringConvertible {
case lrclib
case musixmatch
case petit
case notReplaced
static var allCases: [LyricsSource] {
return [.genius, .lrclib, .musixmatch, .petit]
}
var description: String {
switch self {
@@ -12,6 +17,15 @@ enum LyricsSource: Int, CaseIterable, CustomStringConvertible {
case .lrclib: "LRCLIB"
case .musixmatch: "Musixmatch"
case .petit: "PetitLyrics"
case .notReplaced: "Spotify"
}
}
var isReplacing: Bool { self != .notReplaced }
static var defaultSource: LyricsSource {
Locale.isInRegion("JP", orHasLanguage: "ja")
? .petit
: .lrclib
}
}
@@ -1,7 +1,6 @@
import Foundation
extension UserDefaults {
private static let defaults = UserDefaults.standard
private static let lyricsSourceKey = "lyricsSource"
@@ -21,11 +20,7 @@ extension UserDefaults {
return LyricsSource(rawValue: rawValue)!
}
if Locale.isInRegion("JP", orHasLanguage: "ja") {
return .petit
}
return .lrclib
return LyricsSource.defaultSource
}
set (newSource) {
defaults.set(newSource.rawValue, forKey: lyricsSourceKey)
@@ -69,7 +69,7 @@ class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDe
}
else {
UserDefaults.patchType = .requests
PremiumPatching().activate()
PremiumPatchingGroup().activate()
}
NSLog("[EeveeSpotify] Fetched bootstrap, \(UserDefaults.patchType) was set")
@@ -6,7 +6,7 @@ private let likedTracksRow: [String: Any] = [
]
class HUBViewModelBuilderImplementationHook: ClassHook<NSObject> {
typealias Group = PremiumPatching
typealias Group = PremiumPatchingGroup
static let targetName: String = "HUBViewModelBuilderImplementation"
func addJSONDictionary(_ dictionary: NSDictionary?) {
@@ -2,7 +2,7 @@ import Orion
import UIKit
class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
typealias Group = PremiumPatching
typealias Group = PremiumPatchingGroup
static let targetName = "StreamQualitySettingsSection"
func shouldResetSelection() -> Bool {
@@ -15,17 +15,8 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
}
}
//
private func showOfflineModePopUp() {
PopUpHelper.showPopUp(
message: "playlist_downloading_popup".localized,
buttonText: "OK".uiKitLocalized
)
}
class ContentOffliningUIHelperImplementationHook: ClassHook<NSObject> {
typealias Group = PremiumPatching
typealias Group = PremiumPatchingGroup
static let targetName = "Offline_ContentOffliningUIImpl.ContentOffliningUIHelperImplementation"
func downloadToggledWithCurrentAvailability(
@@ -34,18 +25,31 @@ class ContentOffliningUIHelperImplementationHook: ClassHook<NSObject> {
removeAction: NSObject,
pageIdentifier: String,
pageURI: URL
) -> String {
if pageIdentifier == "spotify:local-files" {
return orig.downloadToggledWithCurrentAvailability(
availability,
addAction: addAction,
removeAction: removeAction,
pageIdentifier: pageIdentifier,
pageURI: pageURI
)
}
) -> String? {
let isPlaylist = [
"free-tier-playlist",
"playlist/ondemand"
].contains(pageIdentifier)
showOfflineModePopUp()
return pageIdentifier
PopUpHelper.showPopUp(
message: "playlist_downloading_popup".localized,
buttonText: "OK".uiKitLocalized,
secondButtonText: isPlaylist
? "download_local_playlist".localized
: nil,
onSecondaryClick: isPlaylist
? {
_ = self.orig.downloadToggledWithCurrentAvailability(
availability,
addAction: addAction,
removeAction: removeAction,
pageIdentifier: pageIdentifier,
pageURI: pageURI
)
}
: nil
)
return nil
}
}
@@ -1,7 +1,7 @@
import Orion
class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook<NSObject> {
typealias Group = PremiumPatching
typealias Group = PremiumPatchingGroup
static let targetName = "SPTFreeTierArtistHubRemoteURLResolver"
func initWithViewURI(
@@ -3,6 +3,7 @@ import UIKit
struct EeveeSettingsView: View {
let navigationController: UINavigationController
static let spotifyAccentColor = Color(hex: "#1ed760")
@State private var hasShownCommonIssuesTip = UserDefaults.hasShownCommonIssuesTip
@State private var isClearingData = false
@@ -18,9 +19,7 @@ struct EeveeSettingsView: View {
init(navigationController: UINavigationController) {
self.navigationController = navigationController
let spotifyAccentColor = UIColor(Color(hex: "#1ed760"))
UIView.appearance().tintColor = spotifyAccentColor
UIView.appearance().tintColor = UIColor(EeveeSettingsView.spotifyAccentColor)
}
var body: some View {
@@ -14,41 +14,15 @@ extension EeveeLyricsSettingsView {
}
@ViewBuilder func LyricsSourceSection() -> some View {
Section(footer: lyricsSourceFooter()) {
Picker(
"lyrics_source".localized,
selection: $lyricsSource
) {
ForEach(LyricsSource.allCases, id: \.self) { lyricsSource in
Text(lyricsSource.description).tag(lyricsSource)
}
}
if lyricsSource == .musixmatch {
VStack(alignment: .leading, spacing: 5) {
Text("musixmatch_user_token".localized)
TextField("user_token_placeholder".localized, text: $musixmatchToken)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Section(footer: Text("restart_is_required_description".localized)) {
Toggle(
"do_not_replace_lyrics".localized,
isOn: Binding<Bool>(
get: { lyricsSource == .notReplaced },
set: { lyricsSource = $0 ? .notReplaced : LyricsSource.defaultSource }
)
)
}
.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)
@@ -57,6 +31,41 @@ extension EeveeLyricsSettingsView {
UserDefaults.lyricsSource = newSource
}
if lyricsSource.isReplacing {
Section(footer: lyricsSourceFooter()) {
Picker(
"lyrics_source".localized,
selection: $lyricsSource
) {
ForEach(LyricsSource.allCases, id: \.self) { lyricsSource in
Text(lyricsSource.description).tag(lyricsSource)
}
}
if lyricsSource == .musixmatch {
VStack(alignment: .leading, spacing: 5) {
Text("musixmatch_user_token".localized)
TextField("user_token_placeholder".localized, text: $musixmatchToken)
.foregroundColor(.gray)
}
.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 = ""
}
}
}
}
}
@@ -11,56 +11,58 @@ struct EeveeLyricsSettingsView: View {
var body: some View {
List {
LyricsSourceSection()
if lyricsSource != .genius {
Section(
footer: Text("genius_fallback_description".localizeWithFormat(lyricsSource.description))
) {
Toggle(
"genius_fallback".localized,
isOn: $geniusFallback
)
if geniusFallback {
if lyricsSource != .notReplaced {
if lyricsSource != .genius {
Section(
footer: Text("genius_fallback_description".localizeWithFormat(lyricsSource.description))
) {
Toggle(
"show_fallback_reasons".localized,
isOn: Binding<Bool>(
get: { UserDefaults.fallbackReasons },
set: { UserDefaults.fallbackReasons = $0 }
)
"genius_fallback".localized,
isOn: $geniusFallback
)
if geniusFallback {
Toggle(
"show_fallback_reasons".localized,
isOn: Binding<Bool>(
get: { UserDefaults.fallbackReasons },
set: { UserDefaults.fallbackReasons = $0 }
)
)
}
}
}
}
//
Section(footer: Text("romanized_lyrics_description".localized)) {
Toggle(
"romanized_lyrics".localized,
isOn: $lyricsOptions.romanization
)
}
if lyricsSource == .musixmatch {
Section {
HStack {
if showLanguageWarning {
Image(systemName: "exclamationmark.triangle")
.font(.title3)
.foregroundColor(.yellow)
//
Section(footer: Text("romanized_lyrics_description".localized)) {
Toggle(
"romanized_lyrics".localized,
isOn: $lyricsOptions.romanization
)
}
if lyricsSource == .musixmatch {
Section {
HStack {
if showLanguageWarning {
Image(systemName: "exclamationmark.triangle")
.font(.title3)
.foregroundColor(.yellow)
}
Text("musixmatch_language".localized)
Spacer()
TextField("en", text: $lyricsOptions.musixmatchLanguage)
.frame(maxWidth: 20)
.foregroundColor(.gray)
}
Text("musixmatch_language".localized)
Spacer()
TextField("en", text: $lyricsOptions.musixmatchLanguage)
.frame(maxWidth: 20)
.foregroundColor(.gray)
} footer: {
Text("musixmatch_language_description".localized)
}
} footer: {
Text("musixmatch_language_description".localized)
}
}
@@ -71,7 +73,6 @@ struct EeveeLyricsSettingsView: View {
.modifier(ListRowSeparatorHidden())
}
}
.listStyle(GroupedListStyle())
.animation(.default, value: lyricsSource)
@@ -7,7 +7,14 @@ struct EeveePatchingSettingsView: View {
var body: some View {
List {
Section(footer: patchType == .disabled ? nil : Text("patching_description".localized)) {
Section(
footer: patchType == .disabled
? nil
: Text(
"patching_description"
.localizeWithFormat("restart_is_required_description".localized)
)
) {
Toggle(
"do_not_patch_premium".localized,
isOn: Binding<Bool>(
@@ -6,44 +6,46 @@ struct EeveeUISettingsView: View {
var body: some View {
List {
Section(
header: Text("lyrics_background_color_section".localized),
footer: Text("lyrics_background_color_section_description".localized)) {
Toggle(
"display_original_colors".localized,
isOn: $lyricsColors.displayOriginalColors
)
Toggle(
"use_static_color".localized,
isOn: $lyricsColors.useStaticColor
)
if lyricsColors.useStaticColor {
ColorPicker(
"static_color".localized,
selection: Binding<Color>(
get: { Color(hex: lyricsColors.staticColor) },
set: { lyricsColors.staticColor = $0.hexString }
),
supportsOpacity: false
if UserDefaults.lyricsSource.isReplacing {
Section(
header: Text("lyrics_background_color_section".localized),
footer: Text("lyrics_background_color_section_description".localized)
) {
Toggle(
"display_original_colors".localized,
isOn: $lyricsColors.displayOriginalColors
)
}
else {
VStack(alignment: .leading, spacing: 5) {
Text("color_normalization_factor".localized)
Slider(
value: $lyricsColors.normalizationFactor,
in: 0.2...0.8,
step: 0.1
Toggle(
"use_static_color".localized,
isOn: $lyricsColors.useStaticColor
)
if lyricsColors.useStaticColor {
ColorPicker(
"static_color".localized,
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".localized)
Slider(
value: $lyricsColors.normalizationFactor,
in: 0.2...0.8,
step: 0.1
)
}
}
}
.onChange(of: lyricsColors) { lyricsColors in
UserDefaults.lyricsColors = lyricsColors
}
}
.onChange(of: lyricsColors) { lyricsColors in
UserDefaults.lyricsColors = lyricsColors
}
Section {
@@ -26,7 +26,7 @@ struct CommonIssuesTipView: View {
.foregroundColor(.white)
+ Text("common_issues_tip_button".localized)
.foregroundColor(.blue)
.foregroundColor(EeveeSettingsView.spotifyAccentColor)
+ Text(".")
.foregroundColor(.white)
+6 -2
View File
@@ -8,7 +8,7 @@ func exitApplication() {
}
}
struct PremiumPatching: HookGroup { }
struct PremiumPatchingGroup: HookGroup { }
struct EeveeSpotify: Tweak {
static let version = "5.5"
@@ -20,7 +20,11 @@ struct EeveeSpotify: Tweak {
}
if UserDefaults.patchType.isPatching {
PremiumPatching().activate()
PremiumPatchingGroup().activate()
}
if UserDefaults.lyricsSource.isReplacing {
LyricsGroup().activate()
}
}
}
@@ -15,18 +15,23 @@ reset_data_description = "Clear cached data and restart the app.";
checking_for_update = "Checking for Update...";
update_available = "Update Available";
restart_is_required_description = "App restart is required after changing.";
// Patching
do_not_patch_premium = "Do Not Patch Premium";
patching_description = "EeveeSpotify 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.";
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.
%@";
overwrite_configuration = "Overwrite Configuration";
overwrite_configuration_description = "Replace remote configuration with the dumped Premium one. This configuration defines most UI/UX parameters and may be helpful, although it could cause issues.";
// Lyrics
do_not_replace_lyrics = "Do Not Replace Lyrics";
lyrics_source = "Lyrics Source";
lyrics_source_description = "You can select the lyrics source you prefer.
@@ -75,7 +80,8 @@ dark_popups = "Dark 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.";
playlist_downloading_popup = "Native playlist downloading is server-sided and is not available with this tweak. You can download podcast episodes and local playlists though.";
download_local_playlist = "Download local playlist";
/* MARK: Lyrics */
@@ -15,18 +15,23 @@ reset_data_description = "Очистить кэшированные данные
checking_for_update = "Проверка наличия обновления...";
update_available = "Доступно обновление";
restart_is_required_description = "Требуется перезапуск для применения.";
// Patching
do_not_patch_premium = "Не патчить Premium";
patching_description = "EeveeSpotify перехватывает запросы на получение данных о пользователе, обрабатывает их и заменяет параметры в реальном времени.
Если у вас есть Premium, вы можете включить Не патчить Premium. Твик не будет изменять данные и ограничивать использование серверных функций. Требуется перезапуск для применения.";
Если у вас есть Premium, вы можете включить Не патчить Premium. Твик не будет изменять данные и ограничивать использование серверных функций.
%@";
overwrite_configuration = "Заменять конфигурацию";
overwrite_configuration_description = "Заменять удаленную конфигурацию на сдампленную конфигурацию Premium. Она определяет многие UI/UX параметры и может быть полезной, хотя может вызвать проблемы.";
// Lyrics
do_not_replace_lyrics = "Не заменять тексты";
lyrics_source = "Источник текстов";
lyrics_source_description = "Вы можете выбрать источник текстов, который Вам больше нравится.
@@ -57,7 +62,7 @@ musixmatch_language_description = "Вы можете ввести двузнач
// UI
lyrics_background_color_section = "Цвет фона текстов";
lyrics_background_color_section_description = "При включении \"Отображать оригинальные цвета\" тексты будут отображаться в оригинальных цветах Spotify у песен, для которых они есть.
lyrics_background_color_section_description = "При включении Отображать оригинальные цвета тексты будут отображаться в оригинальных цветах Spotify у песен, для которых они есть.
Вы можете выбрать статический цвет или уровень нормализации на основе цвета обложки песни. Он определяет, насколько темные цвета осветлены, а светлые — затемнены. В основном чем выше уровень нормализации, тем цвета светлее.";
@@ -73,7 +78,8 @@ dark_popups = "Темные сообщения";
have_premium_popup = "Похоже, у Вас есть активная подписка Premium. Твик не будет изменять данные и ограничивать использование серверных функций. Вы можете управлять этим в настройках EeveeSpotify.";
high_audio_quality_popup = "Очень высокое качество недоступно с твиком, так как предоставляется на стороне сервера.";
playlist_downloading_popup = "Встроенная загрузка плейлистов осуществляется из сервера и недоступна с твиком. Однако вы можете загружать эпизоды подкастов.";
playlist_downloading_popup = "Встроенная загрузка плейлистов осуществляется из сервера и недоступна с твиком. Однако вы можете загружать эпизоды подкастов и локальные плейлисты.";
download_local_playlist = "Загрузить локальный плейлист";
/* MARK: Lyrics */