Main application window instance used for authentication flows
export function setupSpotifyIPC(mainWindow: BrowserWindow): void {
// Authentication handlers
ipcMain.handle(
"spotify:authenticate",
async (_, credentials, forceAuth = false) => {
try {
console.log(
"Authenticating with Spotify...",
credentials,
forceAuth ? "(forced)" : "",
);
// Set shared credentials in the API first
setCredentials(credentials.clientId, credentials.clientSecret);
// Clear tokens if force authentication is requested
if (forceAuth) {
clearStoredTokens();
// Also clear browser cookies and storage for Spotify
try {
await clearSpotifyAuthData();
} catch (error) {
saveLog(`Error clearing Spotify auth data: ${error}`, "ERROR");
}
saveLog(
"Forcing new authentication flow with cleared tokens and cookies",
"DEBUG",
);
}
// Skip stored token check if force authentication is requested
else if (!forceAuth) {
// Attempt to use stored tokens
const storedTokens = loadTokens();
if (storedTokens) {
try {
const now = Date.now();
const expiresIn = Math.max(
0,
(storedTokens.expiresAt - now) / 1000,
);
// If token has already expired or is about to expire in less than 2 minutes,
// don't try to use it and go directly to refresh
if (expiresIn > 120) {
// Set valid tokens in API service
setSpotifyApiTokens(
storedTokens.accessToken,
storedTokens.refreshToken,
Math.floor(expiresIn),
);
saveLog("Using existing valid tokens from storage", "DEBUG");
// Verify token with a quick API call
try {
await axios.get("https://api.spotify.com/v1/me", {
headers: {
Authorization: `Bearer ${storedTokens.accessToken}`,
},
});
saveLog("Verified token is valid with Spotify API", "DEBUG");
return true;
} catch (apiError) {
saveLog(
`Token validation failed, refreshing: ${apiError}`,
"DEBUG",
);
// Fall through to token refresh
}
} else {
saveLog(
`Token expired or about to expire (${expiresIn}s remaining), refreshing`,
"DEBUG",
);
}
// Refresh expired token
await refreshSpotifyToken();
// Update stored tokens
const tokenInfo = getSpotifyTokenInfo();
if (tokenInfo.hasAccessToken && tokenInfo.hasRefreshToken) {
// We need the actual values here since the token info doesn't contain the actual tokens
const loadedTokens = loadTokens();
if (loadedTokens) {
saveTokens({
accessToken: getAccessToken() || loadedTokens.accessToken,
refreshToken:
getRefreshToken() || loadedTokens.refreshToken,
expiresAt: Date.now() + tokenInfo.expiresIn * 1000,
});
}
}
} catch (error) {
console.error("Error using stored tokens:", error);
// Fall through to OAuth flow on token error
}
}
} else {
saveLog("Forcing new authentication flow", "DEBUG");
}
// Initiate new authentication flow
try {
const tokens = await startAuthFlow(
mainWindow,
credentials.clientId,
credentials.clientSecret,
credentials.redirectUri,
forceAuth,
);
// Persist tokens
saveTokens({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + tokens.expiresIn * 1000,
});
// Initialize API service with tokens
setApiTokens(
tokens.accessToken,
tokens.refreshToken,
tokens.expiresIn,
);
saveLog("Successfully authenticated with Spotify", "INFO");
return true;
} catch (error) {
saveLog(`Authentication failed: ${error}`, "ERROR");
return false;
}
} catch (error) {
saveLog(`Authentication error: ${error}`, "ERROR");
return false;
}
},
);
// Logout handler
ipcMain.handle("spotify:logout", async () => {
console.log("Logging out from Spotify");
// Terminate active monitoring
if (isMonitoringActive()) {
stopPlaybackMonitoring();
}
// Clear authentication state
clearStoredTokens();
// Clear cookies for Spotify domains to ensure proper logout
try {
await clearSpotifyAuthData();
} catch (error) {
saveLog(`Error clearing Spotify cookies: ${error}`, "ERROR");
}
saveLog("Logged out from Spotify", "INFO");
return true;
});
// Authentication status verification
ipcMain.handle("spotify:isAuthenticated", async () => {
try {
const storedTokens = loadTokens();
const settings = getSettings();
// Ensure credentials are set
setCredentials(settings.clientId, settings.clientSecret);
if (storedTokens) {
// Initialize API with stored tokens
const now = Date.now();
const expiresIn = Math.max(0, (storedTokens.expiresAt - now) / 1000);
setSpotifyApiTokens(
storedTokens.accessToken,
storedTokens.refreshToken,
Math.floor(expiresIn),
);
// If token has already expired or is about to expire in less than 2 minutes,
// don't try to use it and go directly to refresh
if (expiresIn > 120) {
// Verify token with a quick API call
try {
await axios.get("https://api.spotify.com/v1/me", {
headers: {
Authorization: `Bearer ${storedTokens.accessToken}`,
},
});
saveLog("Verified token is valid with Spotify API", "DEBUG");
return true;
} catch (apiError) {
saveLog(
`Token validation failed, attempting refresh: ${apiError}`,
"DEBUG",
);
// Fall through to token refresh
}
} else {
saveLog(
`Token expired or about to expire (${expiresIn}s remaining), refreshing`,
"DEBUG",
);
}
// Attempt token refresh
try {
if (hasCredentials()) {
await refreshSpotifyToken();
// Update token storage with the newest tokens
const tokenInfo = getSpotifyTokenInfo();
if (tokenInfo.hasAccessToken && tokenInfo.hasRefreshToken) {
saveTokens({
accessToken: getAccessToken() || storedTokens.accessToken,
refreshToken: getRefreshToken() || storedTokens.refreshToken,
expiresAt: Date.now() + tokenInfo.expiresIn * 1000,
});
saveLog("Successfully refreshed and saved tokens", "DEBUG");
return true;
}
} else {
saveLog("Cannot refresh token: Credentials not set", "WARNING");
}
} catch (error) {
saveLog(`Failed to refresh token: ${error}`, "WARNING");
}
}
return false;
} catch (error) {
saveLog(`Error checking authentication status: ${error}`, "ERROR");
return false;
}
});
// Playback information retrieval
ipcMain.handle("spotify:getCurrentPlayback", async () => {
try {
const settings = getSettings();
// Set credentials before making the call
setCredentials(settings.clientId, settings.clientSecret);
// Call with new signature
return await getCurrentPlayback(true);
} catch (error) {
saveLog(`Error getting current playback: ${error}`, "ERROR");
return null;
}
});
// Skipped tracks management
ipcMain.handle("spotify:getSkippedTracks", async () => {
try {
// Get the timeframe setting
const settings = getSettings();
const timeframeInDays = settings.timeframeInDays || 0;
// Get all tracks first
const allTracks = getSkippedTracks();
// Apply timeframe filter if needed
const tracks =
timeframeInDays > 0
? filterSkippedTracksByTimeframe(timeframeInDays)
: allTracks;
saveLog(
`Loaded ${tracks.length} skipped tracks from storage (filtered by ${timeframeInDays} day timeframe)`,
"DEBUG",
);
return tracks;
} catch (error) {
saveLog(`Error loading skipped tracks: ${error}`, "ERROR");
return [];
}
});
// Add a handler specifically for refreshing skipped tracks data
ipcMain.handle("spotify:refreshSkippedTracks", async () => {
try {
// Get the timeframe setting
const settings = getSettings();
const timeframeInDays = settings.timeframeInDays || 0;
// Get all tracks first
const allTracks = getSkippedTracks();
// Apply timeframe filter if needed
const tracks =
timeframeInDays > 0
? filterSkippedTracksByTimeframe(timeframeInDays)
: allTracks;
return tracks;
} catch (error) {
saveLog(`Error refreshing skipped tracks: ${error}`, "ERROR");
return [];
}
});
ipcMain.handle("spotify:saveSkippedTracks", async (_, tracks) => {
console.log("Saving skipped tracks...", tracks.length);
const result = saveSkippedTracks(tracks);
saveLog(`Saved ${tracks.length} skipped tracks to storage`, "DEBUG");
return result;
});
ipcMain.handle("spotify:updateSkippedTrack", async (_, track) => {
console.log("Updating skipped track...", track.id);
const result = updateSkippedTrack(track);
saveLog(
`Updated skipped track: ${track.name} by ${track.artist} (ID: ${track.id})`,
"DEBUG",
);
return result;
});
ipcMain.handle("spotify:removeFromSkippedData", async (_, trackId) => {
console.log("Removing track from skipped data:", trackId);
try {
const result = removeSkippedTrack(trackId);
if (!result) {
saveLog(
`Failed to remove track ${trackId} from skipped data`,
"WARNING",
);
}
return result;
} catch (error) {
saveLog(
`Error removing track ${trackId} from skipped data: ${error}`,
"ERROR",
);
return false;
}
});
// Library management
ipcMain.handle("spotify:unlikeTrack", async (_, trackId) => {
console.log("Unliking track from Spotify:", trackId);
try {
const settings = getSettings();
// Set credentials before making the call
setCredentials(settings.clientId, settings.clientSecret);
// Call with new signature
const result = await unlikeTrack(trackId);
if (result) {
saveLog(
`Successfully unliked track ${trackId} from Spotify library`,
"INFO",
);
} else {
saveLog(
`Failed to unlike track ${trackId} from Spotify library`,
"WARNING",
);
}
return result;
} catch (error) {
saveLog(`Error unliking track ${trackId}: ${error}`, "ERROR");
return false;
}
});
// Settings persistence
ipcMain.handle("spotify:saveSettings", async (_, settings) => {
const result = saveSettings(settings);
if (result) {
saveLog("Settings saved successfully", "DEBUG");
} else {
saveLog("Failed to save settings", "ERROR");
}
return result;
});
ipcMain.handle("spotify:getSettings", async () => {
const settings = getSettings();
saveLog("Settings loaded from storage", "DEBUG");
return settings;
});
ipcMain.handle("spotify:resetSettings", async () => {
const result = resetSettings();
if (result) {
saveLog("Settings reset to defaults successfully", "INFO");
} else {
saveLog("Failed to reset settings to defaults", "ERROR");
}
return result;
});
// Logging system
ipcMain.handle("spotify:saveLog", async (_, message, level = "INFO") => {
console.log(`Saving log [${level}]:`, message);
return saveLog(message, level);
});
ipcMain.handle("spotify:getLogs", async (_, count) => {
return getLogs(count);
});
ipcMain.handle("spotify:getAvailableLogFiles", async () => {
return getAvailableLogFiles();
});
ipcMain.handle("spotify:getLogsFromFile", async (_, fileName, count) => {
return getLogsFromFile(fileName, count);
});
ipcMain.handle("spotify:clearLogs", async () => {
console.log("Clearing logs");
return clearLogs();
});
// File system access
ipcMain.handle("spotify:openLogsDirectory", async () => {
console.log("Opening logs directory:", logsPath);
try {
const error = await shell.openPath(logsPath);
if (error) {
console.error("Failed to open logs directory:", error);
saveLog(`Failed to open logs directory: ${error}`, "ERROR");
return false;
}
saveLog("Opened logs directory", "INFO");
return true;
} catch (error) {
console.error("Error opening logs directory:", error);
saveLog(`Error opening logs directory: ${error}`, "ERROR");
return false;
}
});
ipcMain.handle("spotify:openSkipsDirectory", async () => {
console.log("Opening skips directory:", skipsPath);
try {
const error = await shell.openPath(skipsPath);
if (error) {
console.error("Failed to open skips directory:", error);
saveLog(`Failed to open skips directory: ${error}`, "ERROR");
return false;
}
saveLog("Opened skips directory", "INFO");
return true;
} catch (error) {
console.error("Error opening skips directory:", error);
saveLog(`Error opening skips directory: ${error}`, "ERROR");
return false;
}
});
// Application lifecycle
ipcMain.handle("spotify:restartApp", async () => {
console.log("Restarting application...");
saveLog("Application restart requested by user", "INFO");
// Delay restart to allow response transmission
setTimeout(() => {
app.relaunch();
app.exit(0);
}, 1000);
return true;
});
// Playback monitoring
ipcMain.handle("spotify:startMonitoring", async () => {
console.log("Starting Spotify monitoring...");
const settings = getSettings();
try {
// First notify UI that we're initializing
mainWindow.webContents.send("spotify:monitoring-status", {
status: "initializing",
message: "Initializing Spotify connection...",
});
// Credentials are set in startPlaybackMonitoring
const success = startPlaybackMonitoring(
mainWindow,
settings.clientId,
settings.clientSecret,
);
if (!success) {
saveLog("Failed to start playback monitoring", "ERROR");
// Notify UI of failure
mainWindow.webContents.send("spotify:monitoring-status", {
status: "error",
message: "Failed to start monitoring",
details: "Check log for details",
});
} else {
// Notify UI of success
mainWindow.webContents.send("spotify:monitoring-status", {
status: "active",
message: "Monitoring active",
});
}
return success;
} catch (error) {
saveLog(`Error starting monitoring: ${error}`, "ERROR");
// Notify UI of error
mainWindow.webContents.send("spotify:monitoring-status", {
status: "error",
message: "Error starting monitoring",
details: String(error),
});
return false;
}
});
ipcMain.handle("spotify:stopMonitoring", async () => {
try {
// Notify UI that we're stopping
mainWindow.webContents.send("spotify:monitoring-status", {
status: "initializing",
message: "Stopping monitoring service...",
});
const success = stopPlaybackMonitoring();
if (success) {
// Notify UI that monitoring stopped
mainWindow.webContents.send("spotify:monitoring-status", {
status: "inactive",
message: "Monitoring stopped",
});
} else {
// Notify UI of failure
mainWindow.webContents.send("spotify:monitoring-status", {
status: "error",
message: "Failed to stop monitoring",
details: "The service may be in an inconsistent state",
});
}
return success;
} catch (error) {
saveLog(`Error stopping monitoring: ${error}`, "ERROR");
// Notify UI of error
mainWindow.webContents.send("spotify:monitoring-status", {
status: "error",
message: "Error stopping monitoring",
details: String(error),
});
return false;
}
});
ipcMain.handle("spotify:isMonitoringActive", async () => {
try {
const active = isMonitoringActive();
// Notify UI of current status
mainWindow.webContents.send("spotify:monitoring-status", {
status: active ? "active" : "inactive",
message: active ? "Monitoring is active" : "Monitoring is inactive",
});
return active;
} catch (error) {
saveLog(`Failed to check monitoring status: ${error}`, "ERROR");
return false;
}
});
// Add a handler for getting detailed monitoring status
ipcMain.handle("spotify:getMonitoringStatus", async () => {
try {
const active = isMonitoringActive();
return {
active,
status: active ? "active" : "inactive",
message: active ? "Monitoring is active" : "Monitoring is inactive",
};
} catch (error) {
saveLog(`Error getting monitoring status: ${error}`, "ERROR");
return {
active: false,
status: "error",
message: "Error checking monitoring status",
details: String(error),
};
}
});
// Playback Control Handlers
ipcMain.handle("spotify:pausePlayback", async () => {
try {
if (!isTokenValid()) {
saveLog(
"Cannot pause playback: Not authenticated with Spotify",
"ERROR",
);
return false;
}
await pause();
saveLog("Paused Spotify playback", "INFO");
return true;
} catch (error) {
saveLog(`Failed to pause playback: ${error}`, "ERROR");
return false;
}
});
ipcMain.handle("spotify:resumePlayback", async () => {
try {
if (!isTokenValid()) {
saveLog(
"Cannot resume playback: Not authenticated with Spotify",
"ERROR",
);
return false;
}
await play();
saveLog("Resumed Spotify playback", "INFO");
return true;
} catch (error) {
saveLog(`Failed to resume playback: ${error}`, "ERROR");
return false;
}
});
ipcMain.handle("spotify:skipToPreviousTrack", async () => {
try {
if (!isTokenValid()) {
saveLog(
"Cannot skip to previous track: Not authenticated with Spotify",
"ERROR",
);
return false;
}
await skipToPrevious();
saveLog("Skipped to previous track", "INFO");
return true;
} catch (error) {
saveLog(`Failed to skip to previous track: ${error}`, "ERROR");
return false;
}
});
ipcMain.handle("spotify:skipToNextTrack", async () => {
try {
if (!isTokenValid()) {
saveLog(
"Cannot skip to next track: Not authenticated with Spotify",
"ERROR",
);
return false;
}
await skipToNext();
saveLog("Skipped to next track", "INFO");
return true;
} catch (error) {
saveLog(`Failed to skip to next track: ${error}`, "ERROR");
return false;
}
});
// Statistics handlers
ipcMain.handle("spotify:getStatistics", async () => {
try {
saveLog("Retrieving statistics data", "DEBUG");
const stats = await getStatistics();
return stats;
} catch (error) {
saveLog(`Error retrieving statistics: ${error}`, "ERROR");
throw new Error(`Failed to get statistics: ${error}`);
}
});
ipcMain.handle("spotify:clearStatistics", async () => {
try {
saveLog("Clearing statistics data", "INFO");
const result = await clearStatistics();
return result;
} catch (error) {
saveLog(`Error clearing statistics: ${error}`, "ERROR");
return false;
}
});
// URL and file system handling
ipcMain.handle("spotify:openURL", async (_, url) => {
try {
console.log("Opening URL in default browser:", url);
// Open URL in default browser
await shell.openExternal(url);
saveLog(`Opened URL in default browser: ${url}`, "INFO");
return true;
} catch (error) {
console.error("Failed to open URL:", error);
saveLog(`Failed to open URL in default browser: ${error}`, "ERROR");
return false;
}
});
ipcMain.handle("spotify:showItemInFolder", async (_, path) => {
try {
console.log("Showing item in folder:", path);
await shell.showItemInFolder(path);
saveLog(`Showed item in folder: ${path}`, "INFO");
return true;
} catch (error) {
console.error("Failed to show item in folder:", error);
saveLog(`Failed to show item in folder: ${error}`, "ERROR");
return false;
}
});
// Add new IPC handlers for dashboard statistics functions
// Get statistics summary for dashboard
ipcMain.handle("spotify:getStatisticsSummary", async () => {
try {
const summary = await getStatisticsSummary();
return summary;
} catch (error) {
saveLog(`Error getting statistics summary: ${error}`, "ERROR");
return {
totalTracks: 0,
totalSkips: 0,
skipPercentage: 0,
todaySkips: 0,
weekSkips: 0,
monthSkips: 0,
avgSkipTime: 0,
};
}
});
// Get recent skipped tracks for dashboard
ipcMain.handle("spotify:getRecentSkippedTracks", async (_, limit = 10) => {
try {
const tracks = await getRecentSkippedTracks(limit);
return tracks;
} catch (error) {
saveLog(`Error getting recent skipped tracks: ${error}`, "ERROR");
return [];
}
});
// Get top skipped artists for dashboard
ipcMain.handle("spotify:getTopSkippedArtists", async (_, limit = 5) => {
try {
const artists = await getTopSkippedArtists(limit);
return artists;
} catch (error) {
saveLog(`Error getting top skipped artists: ${error}`, "ERROR");
return [];
}
});
// Get recent sessions for dashboard
ipcMain.handle("spotify:getRecentSessions", async (_, limit = 3) => {
try {
const sessions = await getRecentSessions(limit);
return sessions;
} catch (error) {
saveLog(`Error getting recent sessions: ${error}`, "ERROR");
return [];
}
});
// Export statistics data
ipcMain.handle("spotify:exportStatistics", async () => {
try {
const success = await exportStatistics();
return success;
} catch (error) {
saveLog(`Error exporting statistics: ${error}`, "ERROR");
return false;
}
});
// Clear all statistics data
ipcMain.handle("spotify:clearAllStatistics", async () => {
try {
const success = await clearAllStatistics();
return success;
} catch (error) {
saveLog(`Error clearing all statistics: ${error}`, "ERROR");
return false;
}
});
}
Configures IPC handlers for Spotify functionality