diff --git a/Lyric Fever.xcodeproj/project.pbxproj b/Lyric Fever.xcodeproj/project.pbxproj index dcd1ae7..c590c0f 100644 --- a/Lyric Fever.xcodeproj/project.pbxproj +++ b/Lyric Fever.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 8F6BD2992A8A6B7D008BBF88 /* viewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6BD2982A8A6B7D008BBF88 /* viewModel.swift */; }; 8FC8E9492A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */; }; 8FC8E94D2A704EED00F69915 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8FC8E94C2A704EED00F69915 /* Assets.xcassets */; }; + 8FCFD1C32AE35DEA00B22023 /* spotifylogin.gif in Resources */ = {isa = PBXBuildFile; fileRef = 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */; }; 8FE454282A8916C30039EFA7 /* SpotifyScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */; }; 8FF59E2E2A798D2B00F0A382 /* Lyrics.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8FF59E2C2A798D2B00F0A382 /* Lyrics.xcdatamodeld */; }; 8FFA9F312AA1B1E600BAEC5C /* OnboardingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFA9F302AA1B1E600BAEC5C /* OnboardingWindow.swift */; }; @@ -35,6 +36,7 @@ 8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyLyricsInMenubarApp.swift; sourceTree = ""; }; 8FC8E94C2A704EED00F69915 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8FC8E9512A704EED00F69915 /* SpotifyLyricsInMenubar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpotifyLyricsInMenubar.entitlements; sourceTree = ""; }; + 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = spotifylogin.gif; sourceTree = ""; }; 8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyScripting.swift; sourceTree = ""; }; 8FE454292A891EBD0039EFA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8FF59E2D2A798D2B00F0A382 /* Lyrics.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Lyrics.xcdatamodel; sourceTree = ""; }; @@ -68,6 +70,7 @@ isa = PBXGroup; children = ( 8F4F495B2A7FB3D400097888 /* SongObject+CoreDataClass.swift */, + 8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */, 8FFA9F362AA1B63500BAEC5C /* crossfade.gif */, 8FFA9F352AA1B63500BAEC5C /* spotifyPermissionMac.gif */, 8F4F495C2A7FB3D400097888 /* SongObject+CoreDataProperties.swift */, @@ -173,6 +176,7 @@ 8FFA9F372AA1B63500BAEC5C /* spotifyPermissionMac.gif in Resources */, 8FC8E94D2A704EED00F69915 /* Assets.xcassets in Resources */, 8FFA9F382AA1B63500BAEC5C /* crossfade.gif in Resources */, + 8FCFD1C32AE35DEA00B22023 /* spotifylogin.gif in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -328,7 +332,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.01; + CURRENT_PROJECT_VERSION = 1.5; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 38TP6LZLJ5; ENABLE_APP_SANDBOX = YES; @@ -347,7 +351,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.5; PRODUCT_BUNDLE_IDENTIFIER = com.aviwadhwa.SpotifyLyricsInMenubar; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -364,7 +368,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.01; + CURRENT_PROJECT_VERSION = 1.5; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 38TP6LZLJ5; ENABLE_APP_SANDBOX = YES; @@ -383,7 +387,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.5; PRODUCT_BUNDLE_IDENTIFIER = com.aviwadhwa.SpotifyLyricsInMenubar; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/SpotifyLyricsInMenubar/OnboardingWindow.swift b/SpotifyLyricsInMenubar/OnboardingWindow.swift index c4ec3bc..e3c15d8 100644 --- a/SpotifyLyricsInMenubar/OnboardingWindow.swift +++ b/SpotifyLyricsInMenubar/OnboardingWindow.swift @@ -25,9 +25,9 @@ struct OnboardingWindow: View { StepView(title: "Make sure Spotify is installed on your mac", description: "Please download the [official Spotify Desktop client](https://www.spotify.com/in-en/download/mac/)") - NavigationLink("Next", destination: FirstView()) + NavigationLink("Next", destination: ZeroView()) .buttonStyle(.borderedProminent) - Text("Email me at [aviwad@gmail.com](mailto:aviwad@gmail.com) for any support\n⚠️ Disclaimer: I do not own the rights to Spotify or the lyric content presented.\nMusixmatch and Spotify own all rights to the lyrics.\nVersion 1.0.1") + Text("Email me at [aviwad@gmail.com](mailto:aviwad@gmail.com) for any support\n⚠️ Disclaimer: I do not own the rights to Spotify or the lyric content presented.\nMusixmatch and Spotify own all rights to the lyrics.\nVersion 1.5") .multilineTextAlignment(.center) .font(.callout) .padding(.top, 10) @@ -36,6 +36,103 @@ struct OnboardingWindow: View { } } +struct ZeroView: View { + @Environment(\.dismiss) var dismiss + @Environment(\.controlActiveState) var controlActiveState + @State var isAnimating = true + @State private var isShowingDetailView = false + @AppStorage("spDcCookie") var spDcCookie: String = "" + @State var isLoading = false + @State var error = false + var body: some View { + VStack(alignment: .leading, spacing: 16) { + StepView(title: "1. Spotify login credentials", description: "We need the cookie to make the lyric api calls.") + + HStack { + Spacer() + AnimatedImage(name: "spotifylogin.gif", isAnimating: $isAnimating) + .resizable() + Spacer() + } + + TextField("Enter your SP_DC Cookie Here :)", text: $spDcCookie) + + HStack { + Button("Back") { + dismiss() + } + Button("Open Spotify on the Web", action: { + let url = URL(string: "https://open.spotify.com")! + NSWorkspace.shared.open(url) + }) + Spacer() + NavigationLink(destination: FirstView(), isActive: $isShowingDetailView) {EmptyView()} + .hidden() + if error && !isLoading { + Text("WRONG SP DC COOKIE TRY AGAIN ⚠️") + .foregroundStyle(.red) + } + if isLoading { + ProgressView() + .scaleEffect(0.5) + .frame(height: 20) + } + Button("Next") { + Task { + isLoading = true + if spDcCookie.count != 159 { + error = true + isLoading = false + } + else if let url = URL(string: "https://open.spotify.com/get_access_token?reason=transport&productType=web_player") { + do { + var request = URLRequest(url: url) + request.setValue("sp_dc=\(spDcCookie)", forHTTPHeaderField: "Cookie") + let accessTokenData = try await URLSession.shared.data(for: request) + print(String(decoding: accessTokenData.0, as: UTF8.self)) + try JSONDecoder().decode(accessTokenJSON.self, from: accessTokenData.0) + print("ACCESS TOKEN IS SAVED") + error = false + isLoading = false + isShowingDetailView = true + } + catch { + self.error = true + isLoading = false + } + } + } + // replace button with spinner + // check if the cookie is legit + // isLoading = false + //isShowingDetailView = true + } + .buttonStyle(.borderedProminent) + .disabled(isLoading) + } + .padding(.vertical, 5) + + } + .padding(.horizontal, 20) + .navigationBarBackButtonHidden(true) +// .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in +// dismiss() +// } + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in + dismiss() + } + .onChange(of: controlActiveState) { newState in + if newState == .inactive { + isAnimating = false + print("inactive") + } else { + isAnimating = true + } + } + } +} + + struct FirstView: View { @Environment(\.dismiss) var dismiss @Environment(\.controlActiveState) var controlActiveState @@ -70,9 +167,11 @@ struct FirstView: View { .navigationBarBackButtonHidden(true) .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in dismiss() + dismiss() } .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in dismiss() + dismiss() } .onChange(of: controlActiveState) { newState in if newState == .inactive { @@ -123,10 +222,12 @@ struct SecondView: View { .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { newValue in dismiss() dismiss() + dismiss() } .onReceive(NotificationCenter.default.publisher(for: NSWindow.willMiniaturizeNotification)) { newValue in dismiss() dismiss() + dismiss() } .onChange(of: controlActiveState) { newState in print(newState) diff --git a/SpotifyLyricsInMenubar/SpotifyLyricsInMenubarApp.swift b/SpotifyLyricsInMenubar/SpotifyLyricsInMenubarApp.swift index 59292be..a9e6dc5 100644 --- a/SpotifyLyricsInMenubar/SpotifyLyricsInMenubarApp.swift +++ b/SpotifyLyricsInMenubar/SpotifyLyricsInMenubarApp.swift @@ -11,6 +11,7 @@ import ServiceManagement struct SpotifyLyricsInMenubarApp: App { @StateObject var viewmodel = viewModel.shared @AppStorage("launchOnLogin") var launchOnLogin: Bool = false + @AppStorage("showLyrics") var showLyrics: Bool = true @AppStorage("hasOnboarded") var hasOnboarded: Bool = false @Environment(\.openWindow) var openWindow var body: some Scene { @@ -19,15 +20,13 @@ struct SpotifyLyricsInMenubarApp: App { Divider() if let currentlyPlaying = viewmodel.currentlyPlaying, let currentlyPlayingName = viewmodel.currentlyPlayingName { Text(!viewmodel.currentlyPlayingLyrics.isEmpty ? "Lyrics Found 😃" : "No Lyrics Found ☹️") - if viewmodel.currentlyPlayingLyrics.isEmpty { - Button("Check For Lyrics Again") { - - Task { - viewmodel.currentlyPlayingLyrics = try await viewmodel.fetchNetworkLyrics(for: currentlyPlaying, currentlyPlayingName) - print("HELLOO") - if viewmodel.isPlaying, !viewmodel.currentlyPlayingLyrics.isEmpty { - viewmodel.startLyricUpdater() - } + Button(viewmodel.currentlyPlayingLyrics.isEmpty ? "Check For Lyrics Again" : "Refresh Lyrics") { + + Task { + viewmodel.currentlyPlayingLyrics = try await viewmodel.fetchNetworkLyrics(for: currentlyPlaying, currentlyPlayingName) + print("HELLOO") + if viewmodel.isPlaying, !viewmodel.currentlyPlayingLyrics.isEmpty { + viewmodel.startLyricUpdater() } } } @@ -42,6 +41,15 @@ struct SpotifyLyricsInMenubarApp: App { launchOnLogin = true } } + Button(showLyrics ? "Don't show lyrics" : "Show lyrics") { + if showLyrics { + showLyrics = false + viewmodel.stopLyricUpdater() + } else { + showLyrics = true + viewmodel.startLyricUpdater() + } + } Divider() Button("Help / Install Guide") { NSApplication.shared.activate(ignoringOtherApps: true) @@ -54,8 +62,11 @@ struct SpotifyLyricsInMenubarApp: App { NSApplication.shared.terminate(nil) }.keyboardShortcut("q") } , label: { - Text(menuBarTitle) + Text(hasOnboarded ? menuBarTitle : "Please Complete Onboarding Process (Click Help)") .onAppear { + if viewmodel.cookie.count != 159 { + hasOnboarded = false + } guard hasOnboarded else { NSApplication.shared.activate(ignoringOtherApps: true) viewmodel.spotifyScript?.name @@ -92,8 +103,11 @@ struct SpotifyLyricsInMenubarApp: App { viewmodel.currentlyPlayingName = currentlyPlayingName } }) + .onChange(of: viewmodel.cookie) { newCookie in + viewmodel.accessToken = nil + } .onChange(of: viewmodel.isPlaying) { nowPlaying in - if nowPlaying { + if nowPlaying, showLyrics { if !viewmodel.currentlyPlayingLyrics.isEmpty { print("timer started for spotify change, lyrics not nil") viewmodel.startLyricUpdater() @@ -132,10 +146,7 @@ struct SpotifyLyricsInMenubarApp: App { } var menuBarTitle: String { - guard hasOnboarded else { - return "Please Complete Onboarding Process (Click Help)" - } - if viewmodel.isPlaying, let currentlyPlayingLyricsIndex = viewmodel.currentlyPlayingLyricsIndex { + if viewmodel.isPlaying, showLyrics, let currentlyPlayingLyricsIndex = viewmodel.currentlyPlayingLyricsIndex { return viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words.trunc(length: 50) } else if let currentlyPlayingName = viewmodel.currentlyPlayingName { return "Now \(viewmodel.isPlaying ? "Playing" : "Paused"): \(currentlyPlayingName.trunc(length: 50))" diff --git a/SpotifyLyricsInMenubar/lyricJsonStruct.swift b/SpotifyLyricsInMenubar/lyricJsonStruct.swift index 7f1d6d4..e73f451 100644 --- a/SpotifyLyricsInMenubar/lyricJsonStruct.swift +++ b/SpotifyLyricsInMenubar/lyricJsonStruct.swift @@ -35,3 +35,14 @@ struct LyricLine: Decodable { self.words = words } } + +// access token json +struct accessTokenJSON: Codable { + let accessToken: String + let accessTokenExpirationTimestampMs: TimeInterval + let isAnonymous: Bool +} + +struct SongObjectParent: Decodable { + let lyrics: SongObject +} diff --git a/SpotifyLyricsInMenubar/viewModel.swift b/SpotifyLyricsInMenubar/viewModel.swift index 4588c7c..475d81e 100644 --- a/SpotifyLyricsInMenubar/viewModel.swift +++ b/SpotifyLyricsInMenubar/viewModel.swift @@ -10,6 +10,7 @@ import ScriptingBridge import CoreData import AmplitudeSwift import Sparkle +import SwiftUI @MainActor class viewModel: ObservableObject { let decoder = JSONDecoder() @@ -26,6 +27,8 @@ import Sparkle @Published var canCheckForUpdates = false private var currentFetchTask: Task<[LyricLine], Error>? private var currentLyricsUpdaterTask: Task? + var accessToken: accessTokenJSON? + @AppStorage("spDcCookie") var cookie = "" init() { updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) @@ -160,21 +163,39 @@ import Sparkle decoder.userInfo[CodingUserInfoKey.trackID] = trackID decoder.userInfo[CodingUserInfoKey.trackName] = trackName decoder.userInfo[CodingUserInfoKey.duration] = TimeInterval(intDuration+10) - if let url = URL(string: "https://spotify-lyric-api.herokuapp.com/?trackid=\(trackID)") { - let urlResponseAndData = try await URLSession.shared.data(from: url) - let songObject = try decoder.decode(SongObject.self, from: urlResponseAndData.0) + /* + check if saved access token is bigger than current time, then continue with url shit + else + check if we have spdc cookie, then access token stuff + then save access token in this observable object + then continue with url shit + otherwise [] + */ + if accessToken == nil || (accessToken!.accessTokenExpirationTimestampMs <= Date().timeIntervalSince1970*1000) { + if let url = URL(string: "https://open.spotify.com/get_access_token?reason=transport&productType=web_player") { + var request = URLRequest(url: url) + request.setValue("sp_dc=\(cookie)", forHTTPHeaderField: "Cookie") + let accessTokenData = try await URLSession.shared.data(for: request) + print(String(decoding: accessTokenData.0, as: UTF8.self)) + accessToken = try JSONDecoder().decode(accessTokenJSON.self, from: accessTokenData.0) + print("ACCESS TOKEN IS SAVED") + } + } + if let accessToken, let url = URL(string: "https://spclient.wg.spotify.com/color-lyrics/v2/track/\(trackID)?format=json&vocalRemoval=false") { + var request = URLRequest(url: url) + request.addValue("WebPlayer", forHTTPHeaderField: "app-platform") + print("the access token is \(accessToken.accessToken)") + request.addValue("Bearer \(accessToken.accessToken)", forHTTPHeaderField: "authorization") + let urlResponseAndData = try await URLSession.shared.data(for: request) + if urlResponseAndData.0.isEmpty { + return [] + } + print(String(decoding: urlResponseAndData.0, as: UTF8.self)) + let songObject = try decoder.decode(SongObjectParent.self, from: urlResponseAndData.0) print("downloaded from internet successfully \(trackID) \(trackName)") saveCoreData() - let lyricsArray = zip(songObject.lyricsTimestamps, songObject.lyricsWords).map { LyricLine(startTime: $0, words: $1) } + let lyricsArray = zip(songObject.lyrics.lyricsTimestamps, songObject.lyrics.lyricsWords).map { LyricLine(startTime: $0, words: $1) } -// if !lyricsArray.isEmpty, let intDuration = spotifyScript?.currentTrack?.duration, let currentlyPlayingName { -// // why + 10? a little buffer to make sure the timer runs a little bit after the song ends -// // if user skips to next song -> doesn't affect us, task cancellation cancels updater -// // if user scrubs or replays the same song -> good for us, the few milliseconds of buffer ensures that we don't accidentally stop the lyric updater -// let duration = TimeInterval(intDuration+10) -// print("appended duration lyric into array for \(trackID) \(trackName)") -// lyricsArray.append(LyricLine(startTime: duration, words: "Now Playing: \(currentlyPlayingName)")) -// } try Task.checkCancellation() amplitude.track(eventType: "Network Fetch") return lyricsArray diff --git a/spotifylogin.gif b/spotifylogin.gif new file mode 100644 index 0000000..9c7d813 Binary files /dev/null and b/spotifylogin.gif differ