Skip to content

Commit

Permalink
feat: enable speaker creation using name as alternative to id
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyhage committed Apr 30, 2023
1 parent ce5fdc8 commit 24d3141
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 30 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.1] - 2023-04-29

### Changed

- A speaker accessory can now be added in the plugin settings using either the Spotify `id` or `name` of the device.

## [1.3.0] - 2023-04-06

### Added
Expand All @@ -26,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Change poblouin references to joeyhage since the old homebridge plugin is no longer maintained

[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.0...HEAD
[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.1...HEAD
[1.3.1]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.2.4...v1.3.0
[1.2.4]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.2.3...v1.2.4
[1.2.3]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/1.2.2...v1.2.3
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

[![npm version](https://img.shields.io/npm/v/homebridge-spotify-speaker)](https://www.npmjs.com/package/homebridge-spotify-speaker) [![npm downloads](https://img.shields.io/npm/dt/homebridge-spotify-speaker)](https://www.npmjs.com/package/homebridge-spotify-speaker) [![Build and Lint](https://github.com/joeyhage/homebridge-spotify-speaker/actions/workflows/build.yml/badge.svg)](https://github.com/joeyhage/homebridge-spotify-speaker/actions/workflows/build.yml)

Forked from poblouin/homebridge-spotify-speaker since it is no longer being maintained.
This is the new home of the official homebridge-spotify-speaker plugin.

## Please read before using and facing any deceptions

Expand Down Expand Up @@ -82,15 +82,23 @@ With the previous steps, you will provide the code grant and the plugin will do
- It will store them in a file named `.homebridge-spotify-speaker` in the homebridge's persist directory. Thus, when your homebridge server restarts, it can fetch back the tokens.
- It will automatically refresh the access token when needed

## Finding a speaker device ID
## Finding a speaker device ID or name

Once the spotify authentication flow is done, the plugin will display the list of available devices in your Homebridge logs. In Homebridge UI, keep an eye on the logs when the plugin restarts and you will see a message looking like the following:

![Example Device Log](assets/example-device.png)

You can then take the `id` from the Spotify device that you want to control and this is what you put in the plugin's configuration as the `spotifyDeviceId`.
### Suggested option

You can also use the [Spotify developer console](https://developer.spotify.com/console/get-users-available-devices/) to get the available devices on your account.
You can then take the `name` from the Spotify device that you want to control and this is what you put in the plugin's configuration as the `spotifyDeviceName`.

This is the suggested option because the device id used by Spotify is prone to change.

### Alternative option

Alternatively, you can take the `id` from the Spotify device that you want to control and put in the plugin's configuration as the `spotifyDeviceId`.

You can also use the [Spotify developer console](https://developer.spotify.com/documentation/web-api/reference/get-a-users-available-devices) to get the available devices on your account.

## Issues and Questions

Expand All @@ -110,10 +118,10 @@ Common issues related to that though could be:

### Amazon Alexa device not responding

Some devices (notably Amazon Alexa devices) will show an `id` like `00000000-0000-0000-0000-000000000000_amzn_1`. However, in some cases, Spotify doesn't like the `_amzn_1` suffix.
Some devices (notably Amazon Alexa devices) will show an `id` like `00000000-0000-0000-0000-000000000000_amzn_1`. However, in some cases, Spotify doesn't like the `_amzn_1` suffix. Try switching to the `spotifyDeviceName` plugin configuration option instead of `spotifyDeviceId`.

To try it before changing the Homebridge plugin settings, test the [Start/Resume Playback API](https://developer.spotify.com/console/put-play/) in the Spotify developer console. Try setting the `device_id` to the `id` with and without the `_amzn_#` suffix.
If you prefer `spotifyDeviceId`, you can test it using the [Start/Resume Playback API](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback) in the Spotify developer console. Try setting the `device_id` to the `spotifyDeviceId` with and without the `_amzn_#` suffix.

## Contributors

Special thanks to [@poblouin](https://github.com/poblouin) who had the original idea for this plugin and did all the heavy lifting! See [poblouin/homebridge-spotify-speaker](https://github.com/poblouin/homebridge-spotify-speaker) for the original repository.
Special thanks to [@poblouin](https://github.com/poblouin) who had the original idea for this plugin and did all the heavy lifting!
10 changes: 8 additions & 2 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,15 @@
},
"spotifyDeviceId": {
"title": "Spotify Device ID",
"description": "The Spotify Device ID. You can find a list of available device in the plugin's logs when it starts or see the README",
"description": "The Spotify Device ID. Either spotifyDeviceId or spotifyDeviceName is required. You can find a list of available devices in the plugin's logs when it starts or see the README",
"type": "string",
"required": true
"required": false
},
"spotifyDeviceName": {
"title": "Spotify Device Name",
"description": "The Spotify Device Name. Either spotifyDeviceId or spotifyDeviceName is required. You can find a list of available devices in the plugin's logs when it starts or see the README",
"type": "string",
"required": false
},
"spotifyPlaylistUrl": {
"title": "Spotify Playlist URL",
Expand Down
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFiles: ['dotenv/config'],
};
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"pre-commit": "lint-staged",
"prepare": "husky install",
"prepublishOnly": "npm run lint && npm run format && npm run build",
"watch": "npm run build && npm link && nodemon"
"watch": "npm run build && npm link && nodemon",
"test": "jest --silent=false"
},
"keywords": [
"homebridge-plugin",
Expand All @@ -54,9 +55,9 @@
"jest": "^29.5.0",
"lint-staged": "^13.2.0",
"nodemon": "^2.0.22",
"prettier": "^2.8.7",
"prettier": "^2.8.8",
"rimraf": "^4.4.1",
"ts-jest": "^29.0.5",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
Expand Down
20 changes: 17 additions & 3 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { URL } from 'url';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { SpotifyApiWrapper } from './spotify-api-wrapper';
import { SpotifySpeakerAccessory } from './spotify-speaker-accessory';
import { HomebridgeSpotifySpeakerDevice, SpotifySpeakerAccessory } from './spotify-speaker-accessory';

const DEVICE_CLASS_CONFIG_MAP = {
speaker: SpotifySpeakerAccessory,
Expand Down Expand Up @@ -76,10 +76,20 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
if (!deviceClass) {
continue;
}
if (!this.deviceConfigurationIsValid(device)) {
this.log.error(
`${
device.deviceName ?? 'unknown device'
} is not configured correctly. See the documentation for initial setup`,
);
continue;
}

const uuid = this.api.hap.uuid.generate(`${device.deviceName}-${device.spotifyDeviceId}`);
const existingAccessory = this.accessories[uuid];
const playlistId = this.extractPlaylistId(device.spotifyPlaylistUrl);
const uuid = this.api.hap.uuid.generate(
`${device.deviceName}-${device.spotifyDeviceId ?? device.spotifyDeviceName}`,
);
const existingAccessory = this.accessories[uuid];

const accessory =
existingAccessory ?? new this.api.platformAccessory(device.deviceName, uuid, deviceClass.CATEGORY);
Expand Down Expand Up @@ -156,4 +166,8 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
this.log.info('Available Spotify devices', spotifyDevices);
}
}

private deviceConfigurationIsValid(device: HomebridgeSpotifySpeakerDevice) {
return device.spotifyDeviceId || device.spotifyDeviceName;
}
}
62 changes: 62 additions & 0 deletions src/spotify-api-wrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { API, Logger, PlatformConfig } from 'homebridge';
import { SpotifyApiWrapper } from './spotify-api-wrapper';

it('should authenticate and persist tokens', async () => {
// given
const wrapper = new SpotifyApiWrapper(
console as Logger,
{
spotifyAuthCode: process.env.SPOTIFY_AUTH_CODE!,
spotifyClientId: process.env.SPOTIFY_CLIENT_ID!,
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
} as unknown as PlatformConfig,
{ user: { persistPath: () => '.' } } as API,
);

// when
const result = await wrapper.authenticate();
wrapper.persistTokens();

// then
expect(result).toBe(true);
});

it('should retrieve device list', async () => {
// given
const wrapper = new SpotifyApiWrapper(
console as Logger,
{
spotifyAuthCode: process.env.SPOTIFY_AUTH_CODE!,
spotifyClientId: process.env.SPOTIFY_CLIENT_ID!,
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
} as unknown as PlatformConfig,
{ user: { persistPath: () => '.' } } as API,
);

// when
await wrapper.authenticate();
const devices = await wrapper.getMyDevices();

// then
expect(devices?.length).toBeGreaterThan(0);
});

it('should retrieve playback state', async () => {
// given
const wrapper = new SpotifyApiWrapper(
console as Logger,
{
spotifyAuthCode: process.env.SPOTIFY_AUTH_CODE!,
spotifyClientId: process.env.SPOTIFY_CLIENT_ID!,
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
} as unknown as PlatformConfig,
{ user: { persistPath: () => '.' } } as API,
);

// when
await wrapper.authenticate();
const state = await wrapper.getPlaybackState();

// then
expect(state?.statusCode).toEqual(200);
});
24 changes: 18 additions & 6 deletions src/spotify-speaker-accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { HomebridgeSpotifySpeakerPlatform } from './platform';
export interface HomebridgeSpotifySpeakerDevice {
deviceName: string;
deviceType: string;
spotifyDeviceId: string;
spotifyDeviceId?: string;
spotifyDeviceName?: string;
spotifyPlaylistUrl: string;
playlistRepeat?: boolean;
playlistShuffle?: boolean;
Expand Down Expand Up @@ -74,11 +75,11 @@ export class SpotifySpeakerAccessory {

try {
if (value) {
await this.platform.spotifyApiWrapper.play(this.device.spotifyDeviceId, this.device.spotifyPlaylistUrl);
await this.platform.spotifyApiWrapper.setShuffle(this.device.playlistShuffle, this.device.spotifyDeviceId);
await this.platform.spotifyApiWrapper.setRepeat(!!this.device.playlistRepeat, this.device.spotifyDeviceId);
await this.platform.spotifyApiWrapper.play(this.device.spotifyDeviceId!, this.device.spotifyPlaylistUrl);
await this.platform.spotifyApiWrapper.setShuffle(this.device.playlistShuffle, this.device.spotifyDeviceId!);
await this.platform.spotifyApiWrapper.setRepeat(!!this.device.playlistRepeat, this.device.spotifyDeviceId!);
} else {
await this.platform.spotifyApiWrapper.pause(this.device.spotifyDeviceId);
await this.platform.spotifyApiWrapper.pause(this.device.spotifyDeviceId!);
}

this.activeState = value;
Expand All @@ -101,7 +102,7 @@ export class SpotifySpeakerAccessory {
}

try {
await this.platform.spotifyApiWrapper.setVolume(value, this.device.spotifyDeviceId);
await this.platform.spotifyApiWrapper.setVolume(value, this.device.spotifyDeviceId!);
this.currentVolume = value;
} catch (error) {
if ((error as Error).name === 'SpotifyDeviceNotFoundError') {
Expand All @@ -119,6 +120,17 @@ export class SpotifySpeakerAccessory {
}

private async setCurrentStates() {
if (this.device.spotifyDeviceName) {
const devices = await this.platform.spotifyApiWrapper.getMyDevices();
const match = devices?.find((device) => device.name === this.device.spotifyDeviceName);
if (match?.id) {
this.device.spotifyDeviceId = match.id;
} else {
this.log.error(
`spotifyDeviceName '${this.device.spotifyDeviceName}' did not match any Spotify devices. spotifyDeviceName is case sensitive.`,
);
}
}
const state = await this.platform.spotifyApiWrapper.getPlaybackState();
const playingHref = state?.body?.context?.href;
const playingDeviceId = state?.body?.device?.id;
Expand Down

0 comments on commit 24d3141

Please sign in to comment.