Skip to content

Commit

Permalink
fix(#51): If shuffle enabled, randomize first track played
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyhage committed Aug 10, 2023
1 parent b5850e4 commit f2a5b41
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 9 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.3] - 2023-08-09

### Fixed

- If shuffle is enabled for the accessory, ensure the first track played is randomized when the accessory is turned on.

## [1.3.2] - 2023-08-08

### Added
Expand Down Expand Up @@ -42,7 +48,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.2...HEAD
[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.3...HEAD
[1.3.2]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.2...v1.3.3
[1.3.2]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.1...v1.3.2
[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
Expand Down
2 changes: 1 addition & 1 deletion src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
}
}

private extractPlaylistId(playlistUrl: string): string | null {
extractPlaylistId(playlistUrl: string): string | null {
try {
// Empty playlist ID is allowed for cases where one wants to only
// play or pause one speaker started from an external source.
Expand Down
34 changes: 30 additions & 4 deletions src/spotify-api-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ export class SpotifyApiWrapper {
}
}

async play(deviceId: string, contextUri: string) {
async play(deviceId: string, contextUri: string, startAtOffset = 0) {
const options = {
device_id: deviceId,
context_uri: contextUri,
offset: { position: startAtOffset },
};

this.logger.debug(`play called with deviceId=${deviceId}, contextUri=${contextUri}`);
this.logger.debug(
`play called with deviceId=${deviceId}, contextUri=${contextUri}. startAtOffset=${startAtOffset}`,
);
await this.wrappedRequest(() => this.spotifyApi.play(options));
}

Expand All @@ -93,14 +96,23 @@ export class SpotifyApiWrapper {
await this.wrappedRequest(() => this.spotifyApi.pause({ device_id: deviceId }));
}

async getPlaylist(contextUri: string) {
this.logger.debug(`getPlaylist called with contextUri=${contextUri}`);
const playlistId = SpotifyApiWrapper.extractPlaylistId(contextUri);
if (playlistId) {
const res = await this.wrappedRequest(() => this.spotifyApi.getPlaylist(playlistId, { fields: 'tracks.total' }));
return res?.body?.tracks?.total ?? 0;
}
}

async getPlaybackState(): Promise<SpotifyPlaybackState | undefined> {
this.logger.debug('getPlaybackState called');
return this.wrappedRequest(() => this.spotifyApi.getMyCurrentPlaybackState());
}

async setShuffle(state: HomebridgeSpotifySpeakerDevice['playlistShuffle'], deviceId: string) {
async setShuffle(state: boolean, deviceId: string) {
this.logger.debug(`setShuffle called with state=${state}, deviceId=${deviceId}`);
await this.wrappedRequest(() => this.spotifyApi.setShuffle(state ?? true, { device_id: deviceId }));
await this.wrappedRequest(() => this.spotifyApi.setShuffle(state, { device_id: deviceId }));
}

async setRepeat(state: HomebridgeSpotifySpeakerDevice['playlistRepeat'], deviceId: string) {
Expand Down Expand Up @@ -232,6 +244,20 @@ export class SpotifyApiWrapper {
this.logger.error('Unexpected error when making request to Spotify:', JSON.stringify(errorMessage));
}
}

static extractPlaylistId(playlistUrl: string): string | null {
try {
// Empty playlist ID is allowed for cases where one wants to only
// play or pause one speaker started from an external source.
if (!playlistUrl) {
return null;
}

return new URL(playlistUrl).pathname.split('/')[2];
} catch (error) {
return null;
}
}
}

const WebApiErrorTypes = ['WebapiError', 'WebapiRegularError', 'WebapiAuthenticationError', 'WebapiPlayerError'];
Expand Down
82 changes: 82 additions & 0 deletions src/spotify-speaker-accessory.it.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as hapNodeJs from 'hap-nodejs';
import { API, Characteristic, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
import { HomebridgeSpotifySpeakerPlatform } from './platform';
import type { PluginLogger } from './plugin-logger';
import { SpotifyApiWrapper } from './spotify-api-wrapper';
import { SpotifySpeakerAccessory } from './spotify-speaker-accessory';

it('set repeat, shuffle, and play', async () => {
// given
const apiWrapper = getSpotifyApiWrapper();

await apiWrapper.authenticate();
SpotifySpeakerAccessory.DEVICES = await apiWrapper.getMyDevices();

// when
const speaker = getSpotifySpeakerAccessory(apiWrapper);
const result = speaker.handleOnSet(true);

// then
clearInterval(speaker.pollInterval);
await expect(result).resolves.toBeUndefined();
});

function getService(): Service {
return {
updateCharacteristic: () => ({} as unknown as Service),
getCharacteristic: () =>
({
onGet: () =>
({
onSet: () => ({} as unknown as Characteristic),
} as unknown as Characteristic),
updateValue: () => ({} as unknown as Characteristic),
} as unknown as Characteristic),
} as unknown as Service;
}

function getPlatformAccessory(): PlatformAccessory {
return { getService } as unknown as PlatformAccessory;
}

function getPlatformConfig(): PlatformConfig {
return {
spotifyAuthCode: process.env.SPOTIFY_AUTH_CODE!,
spotifyClientId: process.env.SPOTIFY_CLIENT_ID!,
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
deviceNotFoundRetry: { enable: false },
} as unknown as PlatformConfig;
}

function getSpotifyApiWrapper(): SpotifyApiWrapper {
return new SpotifyApiWrapper(console as unknown as PluginLogger, getPlatformConfig(), {
user: { persistPath: () => '.' },
} as API);
}

function getSpotifySpeakerAccessory(apiWrapper: SpotifyApiWrapper): SpotifySpeakerAccessory {
const logger = console as unknown as PluginLogger;
return new SpotifySpeakerAccessory(
{
Characteristic: hapNodeJs.Characteristic,
Service: hapNodeJs.Service,
accessories: {},
api: {},
config: getPlatformConfig(),
log: console as Logger,
logger,
pollIntervalSec: 20,
spotifyApiWrapper: apiWrapper,
} as unknown as HomebridgeSpotifySpeakerPlatform,
getPlatformAccessory(),
{
deviceName: 'test',
deviceType: 'speaker',
spotifyDeviceName: process.env.SPOTIFY_DEVICE_NAME!,
spotifyPlaylistUrl: process.env.SPOTIFY_PLAYLIST_URL!,
playlistRepeat: true,
playlistShuffle: true,
},
logger,
);
}
23 changes: 20 additions & 3 deletions src/spotify-speaker-accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export class SpotifySpeakerAccessory {
private activeState: boolean;
private currentVolume: number;

public readonly pollInterval: NodeJS.Timer;

public static CATEGORY = Categories.LIGHTBULB;
public static DEVICES: SpotifyApi.UserDevice[] = [];
public static CURRENT_STATE: SpotifyApi.CurrentPlaybackResponse | undefined = undefined;
Expand Down Expand Up @@ -39,7 +41,7 @@ export class SpotifySpeakerAccessory {

this.setInitialState();

setInterval(async () => {
this.pollInterval = setInterval(async () => {
const oldActiveState = this.activeState;
const oldVolume = this.currentVolume;

Expand Down Expand Up @@ -67,8 +69,14 @@ 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!);
const doShuffle = this.device.playlistShuffle ?? true;
const offset = await this.chooseRandomOffset(doShuffle);
await this.platform.spotifyApiWrapper.play(
this.device.spotifyDeviceId!,
this.device.spotifyPlaylistUrl,
offset,
);
await this.platform.spotifyApiWrapper.setShuffle(doShuffle, this.device.spotifyDeviceId!);
await this.platform.spotifyApiWrapper.setRepeat(!!this.device.playlistRepeat, this.device.spotifyDeviceId!);
} else {
await this.platform.spotifyApiWrapper.pause(this.device.spotifyDeviceId!);
Expand Down Expand Up @@ -167,4 +175,13 @@ export class SpotifySpeakerAccessory {

return false;
}

private async chooseRandomOffset(doShuffle: boolean) {
if (doShuffle) {
const trackCount = (await this.platform.spotifyApiWrapper.getPlaylist(this.device.spotifyPlaylistUrl)) ?? 0;
return Math.floor(Math.random() * trackCount);
} else {
return 0;
}
}
}

0 comments on commit f2a5b41

Please sign in to comment.