From 21b3267ac3475becaf7d0d3463325563b722d687 Mon Sep 17 00:00:00 2001 From: eevee Date: Thu, 7 Nov 2024 00:49:54 +0300 Subject: [PATCH] musixmatch anonymous tokens --- .../EeveeSpotify/Lyrics/CustomLyrics.x.swift | 14 ++++----- .../Lyrics/Models/LyricsError.swift | 28 ++++++++--------- .../Repositories/GeniusLyricsRepository.swift | 8 ++--- .../Repositories/LrclibLyricsRepository.swift | 4 +-- .../MusixmatchLyricsRepository.swift | 20 ++++++------- .../Repositories/PetitLyricsRepository.swift | 12 ++++---- .../Extensions/UIDevice+Extension.swift | 6 ++++ .../Helpers/AnonymousTokenHelper.swift | 22 ++++++++++++++ .../Settings/Models/AnonymousTokenError.swift | 3 ++ .../EeveeLyricsSettingsView+Extension.swift | 30 +++++++++++++++++-- ...ricsSettingsView+LyricsSourceSection.swift | 2 +- .../Sections/EeveeLyricsSettingsView.swift | 19 +++++++----- .../en.lproj/Localizable.strings | 3 ++ .../ru.lproj/Localizable.strings | 3 ++ 14 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift create mode 100644 Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift index 30f9f242..5fe248b8 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift @@ -126,7 +126,7 @@ class LyricsOnlyViewControllerHook: ClassHook { private func loadLyricsForCurrentTrack() throws { guard let track = HookedInstances.currentTrack else { - throw LyricsError.NoCurrentTrack + throw LyricsError.noCurrentTrack } // @@ -145,7 +145,7 @@ private func loadLyricsForCurrentTrack() throws { case .lrclib: LrcLibLyricsRepository() case .musixmatch: MusixmatchLyricsRepository.shared case .petit: PetitLyricsRepository() - case .notReplaced: throw LyricsError.InvalidSource + case .notReplaced: throw LyricsError.invalidSource } let lyricsDto: LyricsDto @@ -163,7 +163,7 @@ private func loadLyricsForCurrentTrack() throws { switch error { - case .InvalidMusixmatchToken: + case .invalidMusixmatchToken: if !hasShownUnauthorizedPopUp { PopUpHelper.showPopUp( delayed: false, @@ -174,7 +174,7 @@ private func loadLyricsForCurrentTrack() throws { hasShownUnauthorizedPopUp.toggle() } - case .MusixmatchRestricted: + case .musixmatchRestricted: if !hasShownRestrictedPopUp { PopUpHelper.showPopUp( delayed: false, @@ -190,7 +190,7 @@ private func loadLyricsForCurrentTrack() throws { } } else { - lastLyricsState.fallbackError = .UnknownError + lastLyricsState.fallbackError = .unknownError } if source == .genius || !UserDefaults.geniusFallback { @@ -221,7 +221,7 @@ private func loadLyricsForCurrentTrack() throws { func getLyricsForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data { guard let track = HookedInstances.currentTrack else { - throw LyricsError.NoCurrentTrack + throw LyricsError.noCurrentTrack } var lyrics = preloadedLyrics @@ -232,7 +232,7 @@ func getLyricsForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data { } guard var lyrics = lyrics else { - throw LyricsError.UnknownError + throw LyricsError.unknownError } let lyricsColorsSettings = UserDefaults.lyricsColors diff --git a/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift b/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift index 70ffcea7..aa44a833 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/LyricsError.swift @@ -1,23 +1,23 @@ import Foundation enum LyricsError: Error, CustomStringConvertible { - case NoCurrentTrack - case MusixmatchRestricted - case InvalidMusixmatchToken - case DecodingError - case NoSuchSong - case UnknownError - case InvalidSource + case noCurrentTrack + case musixmatchRestricted + case invalidMusixmatchToken + case decodingError + case noSuchSong + case unknownError + case invalidSource var description: String { switch self { - case .NoSuchSong: "no_such_song".localized - case .MusixmatchRestricted: "musixmatch_restricted".localized - case .InvalidMusixmatchToken: "invalid_musixmatch_token".localized - case .DecodingError: "decoding_error".localized - case .NoCurrentTrack: "no_current_track".localized - case .UnknownError: "unknown_error".localized - case .InvalidSource: "" + case .noSuchSong: "no_such_song".localized + case .musixmatchRestricted: "musixmatch_restricted".localized + case .invalidMusixmatchToken: "invalid_musixmatch_token".localized + case .decodingError: "decoding_error".localized + case .noCurrentTrack: "no_current_track".localized + case .unknownError: "unknown_error".localized + case .invalidSource: "" } } } diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift index 3d88a685..85f83a5a 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift @@ -53,7 +53,7 @@ struct GeniusLyricsRepository: LyricsRepository { } guard let rootResponse = try? jsonDecoder.decode(GeniusRootResponse.self, from: data!) else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } return rootResponse.response } @@ -67,7 +67,7 @@ struct GeniusLyricsRepository: LyricsRepository { case .sections(let sectionsResponse) = data, let section = sectionsResponse.sections.first else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } return section.hits @@ -77,7 +77,7 @@ struct GeniusLyricsRepository: LyricsRepository { let data = try perform("/songs/\(songId)", query: ["text_format": "plain"]) guard case .song(let songResponse) = data else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } return songResponse.song @@ -131,7 +131,7 @@ struct GeniusLyricsRepository: LyricsRepository { let hits = try searchSong("\(strippedTitle) \(query.primaryArtist)") guard !hits.isEmpty else { - throw LyricsError.NoSuchSong + throw LyricsError.noSuchSong } var hasFoundRomanizedLyrics = false diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift index 6aa347d6..5563b6b5 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift @@ -97,7 +97,7 @@ struct LrcLibLyricsRepository: LyricsRepository { let songs = try searchSong("\(strippedTitle) \(query.primaryArtist)") guard let song = mostRelevantSong(songs: songs, strippedTitle: strippedTitle) else { - throw LyricsError.NoSuchSong + throw LyricsError.noSuchSong } if let syncedLyrics = song.syncedLyrics { @@ -110,7 +110,7 @@ struct LrcLibLyricsRepository: LyricsRepository { } guard let plainLyrics = song.plainLyrics else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } let lines = Array(plainLyrics.components(separatedBy: "\n").dropLast()) diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift index d1cf2495..d4582fb2 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/MusixmatchLyricsRepository.swift @@ -22,9 +22,7 @@ class MusixmatchLyricsRepository: LyricsRepository { var finalQuery = query finalQuery["usertoken"] = UserDefaults.musixmatchToken - finalQuery["app_id"] = UIDevice.current.isIpad - ? "mac-ios-ipad-v1.0" - : "mac-ios-v2.0" + finalQuery["app_id"] = UIDevice.current.musixmatchAppId let queryString = finalQuery.queryString.addingPercentEncoding( withAllowedCharacters: .urlHostAllowed @@ -63,12 +61,12 @@ class MusixmatchLyricsRepository: LyricsRepository { let body = message["body"] as? [String: Any], let macroCalls = body["macro_calls"] as? [String: Any] else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } if let header = message["header"] as? [String: Any], header["status_code"] as? Int == 401 { - throw LyricsError.InvalidMusixmatchToken + throw LyricsError.invalidMusixmatchToken } return macroCalls @@ -81,11 +79,11 @@ class MusixmatchLyricsRepository: LyricsRepository { let firstSubtitle = subtitleList.first, let subtitle = firstSubtitle["subtitle"] as? [String: Any] else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } if let restricted = subtitle["restricted"] as? Bool, restricted { - throw LyricsError.MusixmatchRestricted + throw LyricsError.musixmatchRestricted } return subtitle @@ -108,7 +106,7 @@ class MusixmatchLyricsRepository: LyricsRepository { let body = message["body"] as? [String: Any], let translationsList = body["translations_list"] as? [[String: Any]] else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } let translations = translationsList.compactMap { @@ -236,7 +234,7 @@ class MusixmatchLyricsRepository: LyricsRepository { let lyricsStatusCode = lyricsHeader["status_code"] as? Int { if lyricsStatusCode == 404 { - throw LyricsError.NoSuchSong + throw LyricsError.noSuchSong } if let lyricsBody = lyricsMessage["body"] as? [String: Any], @@ -245,7 +243,7 @@ class MusixmatchLyricsRepository: LyricsRepository { let plainLyrics = lyrics["lyrics_body"] as? String { if let restricted = lyrics["restricted"] as? Bool, restricted { - throw LyricsError.MusixmatchRestricted + throw LyricsError.musixmatchRestricted } return LyricsDto( @@ -259,6 +257,6 @@ class MusixmatchLyricsRepository: LyricsRepository { } } - throw LyricsError.DecodingError + throw LyricsError.decodingError } } diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift index 2bb0f3ec..f4415b7a 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift @@ -44,7 +44,7 @@ struct PetitLyricsRepository: LyricsRepository { } guard let response = try? XMLDecoder().decode(PetitResponse.self, from: data!) else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } return response @@ -58,7 +58,7 @@ struct PetitLyricsRepository: LyricsRepository { ) guard let song = response.songs.first else { - throw LyricsError.NoSuchSong + throw LyricsError.noSuchSong } return song @@ -81,7 +81,7 @@ struct PetitLyricsRepository: LyricsRepository { ) guard let song = response.songs.first else { - throw LyricsError.NoSuchSong + throw LyricsError.noSuchSong } return song @@ -97,7 +97,7 @@ struct PetitLyricsRepository: LyricsRepository { ) guard let lyricsData = Data(base64Encoded: song.lyricsData) else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } switch song.lyricsType { @@ -105,7 +105,7 @@ struct PetitLyricsRepository: LyricsRepository { case .wordsSynced: guard let lyrics = try? XMLDecoder().decode(PetitLyricsData.self, from: lyricsData) else { - throw LyricsError.DecodingError + throw LyricsError.decodingError } return LyricsDto( @@ -132,7 +132,7 @@ struct PetitLyricsRepository: LyricsRepository { ) default: - throw LyricsError.DecodingError + throw LyricsError.decodingError } } } diff --git a/Sources/EeveeSpotify/Models/Extensions/UIDevice+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/UIDevice+Extension.swift index 90dc5521..ef9a7ee5 100644 --- a/Sources/EeveeSpotify/Models/Extensions/UIDevice+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/UIDevice+Extension.swift @@ -4,4 +4,10 @@ extension UIDevice { var isIpad: Bool { self.userInterfaceIdiom == .pad } + + var musixmatchAppId: String { + UIDevice.current.isIpad + ? "mac-ios-ipad-v1.0" + : "mac-ios-v2.0" + } } diff --git a/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift b/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift new file mode 100644 index 00000000..6aa0d4f1 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift @@ -0,0 +1,22 @@ +import Foundation +import UIKit + +struct AnonymousTokenHelper { + private static let apiUrl = "https://apic.musixmatch.com" + + static func requestAnonymousMusixmatchToken() async throws -> String { + let url = URL(string: "\(apiUrl)/ws/1.1/token.get?app_id=\(await UIDevice.current.musixmatchAppId)")! + let (data, _) = try await URLSession.shared.data(from: url) + + guard + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let message = json["message"] as? [String: Any], + let body = message["body"] as? [String: Any], + let userToken = body["user_token"] as? String + else { + throw AnonymousTokenError.invalidResponse + } + + return userToken + } +} diff --git a/Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift b/Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift new file mode 100644 index 00000000..7d311a65 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift @@ -0,0 +1,3 @@ +enum AnonymousTokenError: Swift.Error { + case invalidResponse +} diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift index dcc2349f..343346ee 100644 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift +++ b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift @@ -13,10 +13,17 @@ extension EeveeLyricsSettingsView { return nil } - func showMusixmatchTokenAlert(_ oldSource: LyricsSource) { + func showMusixmatchTokenAlert(_ oldSource: LyricsSource, showAnonymousTokenOption: Bool) { + var message = "enter_user_token_message".localized + + if showAnonymousTokenOption { + message.append("\n\n") + message.append("request_anonymous_token_description".localized) + } + let alert = UIAlertController( title: "enter_user_token".localized, - message: "enter_user_token_message".localized, + message: message, preferredStyle: .alert ) @@ -27,6 +34,25 @@ extension EeveeLyricsSettingsView { alert.addAction(UIAlertAction(title: "Cancel".uiKitLocalized, style: .cancel) { _ in lyricsSource = oldSource }) + + if showAnonymousTokenOption { + alert.addAction(UIAlertAction(title: "request_anonymous_token".localized, style: .default) { _ in + Task { + defer { + isRequestingMusixmatchToken.toggle() + } + do { + isRequestingMusixmatchToken.toggle() + + musixmatchToken = try await AnonymousTokenHelper.requestAnonymousMusixmatchToken() + UserDefaults.lyricsSource = .musixmatch + } + catch { + showMusixmatchTokenAlert(oldSource, showAnonymousTokenOption: false) + } + } + }) + } alert.addAction(UIAlertAction(title: "OK".uiKitLocalized, style: .default) { _ in let text = alert.textFields!.first!.text! diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift index f0deff01..fbf1925e 100644 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift +++ b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift @@ -25,7 +25,7 @@ extension EeveeLyricsSettingsView { } .onChange(of: lyricsSource) { [lyricsSource] newSource in if newSource == .musixmatch && musixmatchToken.isEmpty { - showMusixmatchTokenAlert(lyricsSource) + showMusixmatchTokenAlert(lyricsSource, showAnonymousTokenOption: true) return } diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift index 50665051..3eefe9b4 100644 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift +++ b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift @@ -4,9 +4,11 @@ struct EeveeLyricsSettingsView: View { @State var musixmatchToken = UserDefaults.musixmatchToken @State var lyricsSource = UserDefaults.lyricsSource @State var geniusFallback = UserDefaults.geniusFallback - @State var lyricsOptions = UserDefaults.lyricsOptions - - @State var showLanguageWarning = false + + @State private var lyricsOptions = UserDefaults.lyricsOptions + + @State var isRequestingMusixmatchToken = false + @State private var isShowingLanguageWarning = false var body: some View { List { @@ -46,7 +48,7 @@ struct EeveeLyricsSettingsView: View { if lyricsSource == .musixmatch { Section { HStack { - if showLanguageWarning { + if isShowingLanguageWarning { Image(systemName: "exclamationmark.triangle") .font(.title3) .foregroundColor(.yellow) @@ -75,8 +77,11 @@ struct EeveeLyricsSettingsView: View { } .listStyle(GroupedListStyle()) + .disabled(isRequestingMusixmatchToken) + .animation(.default, value: lyricsSource) - .animation(.default, value: showLanguageWarning) + .animation(.default, value: isRequestingMusixmatchToken) + .animation(.default, value: isShowingLanguageWarning) .animation(.default, value: geniusFallback) .onChange(of: geniusFallback) { geniusFallback in @@ -87,7 +92,7 @@ struct EeveeLyricsSettingsView: View { let selectedLanguage = lyricsOptions.musixmatchLanguage if selectedLanguage.isEmpty || selectedLanguage ~= "^[\\w\\d]{2}$" { - showLanguageWarning = false + isShowingLanguageWarning = false MusixmatchLyricsRepository.shared.selectedLanguage = selectedLanguage UserDefaults.lyricsOptions = lyricsOptions @@ -95,7 +100,7 @@ struct EeveeLyricsSettingsView: View { return } - showLanguageWarning = true + isShowingLanguageWarning = true } } } diff --git a/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings b/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings index 3f96383d..34028cec 100644 --- a/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings +++ b/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings @@ -110,3 +110,6 @@ let_the_music_play = "Let the music play..."; liked_songs = "Liked songs"; contributors = "Contributors"; + +request_anonymous_token = "Request Anonymous Token"; +request_anonymous_token_description = "Tap “Request Anonymous Token,” so EeveeSpotify will try to request the token from Musixmatch without authorization."; diff --git a/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings b/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings index 084fe811..39173af2 100644 --- a/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings +++ b/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings @@ -108,3 +108,6 @@ let_the_music_play = "Пусть заиграет музыка..."; liked_songs = "Тебе понравилось"; contributors = "Участники"; + +request_anonymous_token = "Запросить анонимный токен"; +request_anonymous_token_description = "Нажмите Запросить анонимный токен, и EeveeSpotify попытается запросить токен у Musixmatch без авторизации.";