Skip to content

Commit

Permalink
Contact Spotify Servers, Refresh Lyrics Button, Hide Lyrics Button
Browse files Browse the repository at this point in the history
New onboarding page to input your API key (directly contacting spotify servers), new refresh lyrics button, new hide lyrics button, new gif image to show how to get Spotify Cookie from web browser
  • Loading branch information
aviwad committed Dec 7, 2023
1 parent 6a544c3 commit 8cee1b5
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 33 deletions.
12 changes: 8 additions & 4 deletions Lyric Fever.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -35,6 +36,7 @@
8FC8E9482A704EEB00F69915 /* SpotifyLyricsInMenubarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyLyricsInMenubarApp.swift; sourceTree = "<group>"; };
8FC8E94C2A704EED00F69915 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8FC8E9512A704EED00F69915 /* SpotifyLyricsInMenubar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpotifyLyricsInMenubar.entitlements; sourceTree = "<group>"; };
8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = spotifylogin.gif; sourceTree = "<group>"; };
8FE454272A8916C30039EFA7 /* SpotifyScripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyScripting.swift; sourceTree = "<group>"; };
8FE454292A891EBD0039EFA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
8FF59E2D2A798D2B00F0A382 /* Lyrics.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Lyrics.xcdatamodel; sourceTree = "<group>"; };
Expand Down Expand Up @@ -68,6 +70,7 @@
isa = PBXGroup;
children = (
8F4F495B2A7FB3D400097888 /* SongObject+CoreDataClass.swift */,
8FCFD1C22AE35DEA00B22023 /* spotifylogin.gif */,
8FFA9F362AA1B63500BAEC5C /* crossfade.gif */,
8FFA9F352AA1B63500BAEC5C /* spotifyPermissionMac.gif */,
8F4F495C2A7FB3D400097888 /* SongObject+CoreDataProperties.swift */,
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
105 changes: 103 additions & 2 deletions SpotifyLyricsInMenubar/OnboardingWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 26 additions & 15 deletions SpotifyLyricsInMenubar/SpotifyLyricsInMenubarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
}
}
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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))"
Expand Down
11 changes: 11 additions & 0 deletions SpotifyLyricsInMenubar/lyricJsonStruct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
45 changes: 33 additions & 12 deletions SpotifyLyricsInMenubar/viewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ScriptingBridge
import CoreData
import AmplitudeSwift
import Sparkle
import SwiftUI

@MainActor class viewModel: ObservableObject {
let decoder = JSONDecoder()
Expand All @@ -26,6 +27,8 @@ import Sparkle
@Published var canCheckForUpdates = false
private var currentFetchTask: Task<[LyricLine], Error>?
private var currentLyricsUpdaterTask: Task<Void,Error>?
var accessToken: accessTokenJSON?
@AppStorage("spDcCookie") var cookie = ""

init() {
updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
Expand Down Expand Up @@ -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
Expand Down
Binary file added spotifylogin.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 8cee1b5

Please sign in to comment.