Skip to content

Commit

Permalink
catch all thrown errors
Browse files Browse the repository at this point in the history
  • Loading branch information
poblouin committed Feb 5, 2022
1 parent 639d917 commit fabd4a6
Show file tree
Hide file tree
Showing 8 changed files with 40 additions and 39 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Spotify Speaker",
"name": "@poblouin/homebridge-spotify-speaker",
"version": "0.0.5-beta",
"version": "0.1.0",
"description": "Homebridge plugin that creates a speaker that plays a specific Spotify playlist",
"license": "MIT",
"author": "Pierre-Olivier Blouin <[email protected]>",
Expand Down
4 changes: 0 additions & 4 deletions src/constants.ts

This file was deleted.

22 changes: 17 additions & 5 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { SpotifySmartSpeakerAccessory } from './spotify-smart-speaker-accessory';
import { SpotifyFakeSpeakerAccessory } from './spotify-speaker-accessory';
import { SpotifyApiWrapper } from './spotify-api-wrapper';
import { SPOTIFY_MISSING_CONFIGURATION_ERROR } from './constants';

const DEVICE_CLASS_CONFIG_MAP = {
speaker: SpotifyFakeSpeakerAccessory,
Expand All @@ -27,12 +26,21 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
this.log.debug('Finished initializing platform:', this.config.name);

if (!config.spotifyClientId || !config.spotifyClientSecret || !config.spotifyAuthCode) {
this.log.error('Missing configuration for this plugin to work, see the documentation for initial setup');
return;
}

this.spotifyApiWrapper = new SpotifyApiWrapper(log, config, api);

this.api.on('didFinishLaunching', async () => {
log.debug('Executed didFinishLaunching callback');

await this.spotifyApiWrapper.authenticate();
const isAuthenticated = await this.spotifyApiWrapper.authenticate();
if (!isAuthenticated) {
return;
}

this.logAvailableSpotifyDevices();
this.discoverDevices();
});
Expand All @@ -51,6 +59,10 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
discoverDevices() {
for (const device of this.config.devices) {
const deviceClass = this.getDeviceConstructor(device.deviceType);
if (!deviceClass) {
continue;
}

const uuid = this.api.hap.uuid.generate(device.spotifyDeviceId);
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid);

Expand All @@ -73,16 +85,16 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
}
}

private getDeviceConstructor(deviceType) {
private getDeviceConstructor(deviceType): typeof SpotifySmartSpeakerAccessory | typeof SpotifyFakeSpeakerAccessory| null {
if (!deviceType) {
this.log.error('It is missing the `deviceType` in the configuration.');
throw new Error(SPOTIFY_MISSING_CONFIGURATION_ERROR);
return null;
}

return DEVICE_CLASS_CONFIG_MAP[deviceType];
}

private async logAvailableSpotifyDevices() {
private async logAvailableSpotifyDevices(): Promise<void> {
const spotifyDevices = await this.spotifyApiWrapper.getMyDevices();
this.log.info('Available Spotify devices', spotifyDevices);
}
Expand Down
43 changes: 18 additions & 25 deletions src/spotify-api-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,16 @@ import fs from 'fs';
import { API, Logger, PlatformConfig } from 'homebridge';
import SpotifyWebApi from 'spotify-web-api-node';

import {
DEFAULT_SPOTIFY_CALLBACK,
SPOTIFY_AUTH_ERROR,
SPOTIFY_MISSING_CONFIGURATION_ERROR,
SPOTIFY_REFRESH_TOKEN_ERROR,
} from './constants';
import { SpotifyPlaybackState, WebapiError } from './types';

const DEFAULT_SPOTIFY_CALLBACK = 'https://example.com/callback';
export class SpotifyApiWrapper {
private readonly authCode: string;
private readonly persistPath: string;

private spotifyApi: SpotifyWebApi;

constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
if (!config.spotifyClientId || !config.spotifyClientSecret || !config.spotifyAuthCode) {
this.log.error('Missing configuration for this plugin to work, see the documentation for initial setup');
throw new Error(SPOTIFY_MISSING_CONFIGURATION_ERROR);
}

this.authCode = config.spotifyAuthCode;
this.persistPath = `${api.user.persistPath()}/.homebridge-spotify-speaker`;

Expand All @@ -33,24 +23,25 @@ export class SpotifyApiWrapper {
});
}

async authenticate() {
async authenticate(): Promise<boolean> {
await this.fetchTokensFromStorage();
if (this.spotifyApi.getAccessToken()) {
this.log.debug('Spotify auth success using saved tokens');
return;
return true;
}

await this.authWithCodeGrant();
if (this.spotifyApi.getAccessToken()) {
this.log.debug('Spotify auth success using authorization code flow');
return;
return true;
}

this.log.error(
`We could not fetch the Spotify tokens nor authenticate using the code grant flow.
Please redo the manual login step, provide the new auth code in the config then try again.`,
);
throw new Error(SPOTIFY_AUTH_ERROR);

return false;
}

persistTokens() {
Expand Down Expand Up @@ -111,7 +102,7 @@ export class SpotifyApiWrapper {
}
}

private async authWithCodeGrant() {
private async authWithCodeGrant(): Promise<void> {
this.log.debug('Attempting the code grant authorization flow');

try {
Expand All @@ -120,7 +111,6 @@ export class SpotifyApiWrapper {
this.spotifyApi.setRefreshToken(data.body['refresh_token']);
} catch (err) {
this.log.error('Could not authorize Spotify:\n\n', err);
throw new Error(SPOTIFY_AUTH_ERROR);
}
}

Expand All @@ -143,16 +133,15 @@ export class SpotifyApiWrapper {
this.spotifyApi.setRefreshToken(tokens.refreshToken);
this.log.debug('Successfully fetched the tokens from storage, going to refresh the access token');

try {
await this.refreshToken();
} catch {
const areTokensRefreshed = await this.refreshTokens();
if (!areTokensRefreshed) {
// Reset the creds since they are wrong, we will try the code auth grant instead.
this.spotifyApi.resetCredentials();
}
}

// TODO: Implement retries, it failed me once.
async refreshToken() {
async refreshTokens(): Promise<boolean> {
try {
const data = await this.spotifyApi.refreshAccessToken();
this.log.debug('The access token has been refreshed!');
Expand All @@ -161,8 +150,10 @@ export class SpotifyApiWrapper {
this.persistTokens();
} catch (err) {
this.log.debug('Could not refresh access token: ', err);
throw new Error(SPOTIFY_REFRESH_TOKEN_ERROR);
return false;
}

return true;
}

// TODO: Use decorator or prettier pattern.
Expand All @@ -174,11 +165,13 @@ export class SpotifyApiWrapper {
if ((error as WebapiError).statusCode === 401) {
this.log.debug('Access token has expired, attempting token refresh');

await this.refreshToken();
return cb();
const areTokensRefreshed = await this.refreshTokens();
if (areTokensRefreshed) {
return cb();
}
}

throw error;
this.log.error('Unexpected error when making a request to Spotify:', error);
}
}
}
2 changes: 1 addition & 1 deletion src/spotify-smart-speaker-accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class SpotifySmartSpeakerAccessory {
}
}, SpotifySmartSpeakerAccessory.DEFAULT_POLL_INTERVAL_MS);

setInterval(() => this.platform.spotifyApiWrapper.refreshToken(), SpotifySmartSpeakerAccessory.DAY_INTERVAL);
setInterval(() => this.platform.spotifyApiWrapper.refreshTokens(), SpotifySmartSpeakerAccessory.DAY_INTERVAL);
}

handleCurrentMediaStateGet(): number {
Expand Down
2 changes: 1 addition & 1 deletion src/spotify-speaker-accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class SpotifyFakeSpeakerAccessory {
}
}, SpotifyFakeSpeakerAccessory.DEFAULT_POLL_INTERVAL_MS);

setInterval(() => this.platform.spotifyApiWrapper.refreshToken(), SpotifyFakeSpeakerAccessory.DAY_INTERVAL);
setInterval(() => this.platform.spotifyApiWrapper.refreshTokens(), SpotifyFakeSpeakerAccessory.DAY_INTERVAL);
}

handleOnGet(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface HomebridgeSpotifySpeakerDevice {
deviceName: string;
deviceStartVolume: number;
deviceType: string;
spotifyDeviceId: string;
spotifyPlaylistId: string;
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"strict": true,
"esModuleInterop": true,
"noImplicitAny": false,
"experimentalDecorators": true
"strictPropertyInitialization": false
},
"include": ["src/"],
"exclude": ["**/*.spec.ts"]
Expand Down

0 comments on commit fabd4a6

Please sign in to comment.