Skip to content

Commit

Permalink
Add chromecast support
Browse files Browse the repository at this point in the history
  • Loading branch information
Niklas Fondberg committed May 1, 2019
1 parent 637c325 commit 7bcf6fb
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 90 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ Web component card which can be used as a Lovelace [Home Assistants](https://www

Forum thread: https://community.home-assistant.io/t/spotify-lovelace-card/103525

This card supports listing the users currently available devices and the users 10 top playlists on [Spotify](https://www.spotify.com).
This card supports listing the users currently available devices and the users top playlists on [Spotify](https://www.spotify.com).
Choose an online media player and click on a playlist to play it on the device.
This component will query the current playback from the Spotify Web API and tries to reflect the current status wrt to device and playlist if something is playing.

The component uses the [Spotify Web API](https://developer.spotify.com/documentation/web-api/).

***New from version 1.5***
The card can make use of [My Spotify Chromecast custom component](https://github.com/fondberg/spotcast) if it is installed, to initiate playback on idle chromecast devices. Please read that README for any limitations.
This release also adds a limit configuration property to make the number of playlists retrieved configurable.

![Screenshot](/spotify-card-highlight.png)

### Requirements
Expand All @@ -31,7 +35,7 @@ Add the resource in lovelace config:
```
- type: module
url: >-
https://cdn.jsdelivr.net/gh/custom-cards/spotify-card@1.4/dist/spotify-card.umd.js
https://cdn.jsdelivr.net/gh/custom-cards/spotify-card@1.5/dist/spotify-card.umd.js
```

##### master version:
Expand All @@ -48,6 +52,7 @@ Now add the card like this:
cards:
- type: 'custom:spotify-card'
client_id: <YOUR CLIENT ID>
limit: <optional number of playlists to retrieve (default 10)>
```

### Improvements to come thru PR or with patience
Expand Down
104 changes: 79 additions & 25 deletions dist/spotify-card.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class PlayerSelect extends preact.Component {
constructor() {
super();
this.state = {
selectedDevice: '-- choose mediaplayer --'
selectedDevice: '-- choose mediaplayer --',
chromecastDevices: []
};
}

Expand All @@ -38,6 +39,17 @@ class PlayerSelect extends preact.Component {
selectedDevice: props.selectedDevice.name
});
}

if (props.hass) {
const chromecastSensor = props.hass.states['sensor.chromecast_devices'];

if (chromecastSensor) {
const chromecastDevices = JSON.parse(chromecastSensor.attributes.devices_json);
this.setState({
chromecastDevices
});
}
}
}

selectDevice(device) {
Expand All @@ -47,11 +59,17 @@ class PlayerSelect extends preact.Component {
this.props.onMediaplayerSelect(device);
}

selectChromecastDevice(device) {
this.props.onChromecastDeviceSelect(device);
}

render() {
const {
devices
} = this.props; // console.log('PlayerSelect: devices', devices);

} = this.props;
const {
chromecastDevices
} = this.state;
return html`
<div class="dropdown">
<div class="mediaplayer_select">
Expand All @@ -71,8 +89,13 @@ class PlayerSelect extends preact.Component {
${this.state.selectedDevice}
</div>
<div class="dropdown-content">
<a onClick=${() => {}}><i>Spotify Connect devices</i></a>
${devices.map((device, idx) => html`
<a onClick=${() => this.selectDevice(device)}>${device.name}</a>
<a onClick=${() => this.selectDevice(device)} style="margin-left: 15px">${device.name}</a>
`)}
<a onClick=${() => {}}><i>Chromecast devices</i></a>
${chromecastDevices.map(chromecastDevice => html`
<a onClick=${() => this.selectChromecastDevice(chromecastDevice)} style="margin-left: 15px">${chromecastDevice.name + ' (' + chromecastDevice.cast_type + ')'}</a>
`)}
</div>
</div>
Expand All @@ -84,6 +107,7 @@ class PlayerSelect extends preact.Component {
class SpotifyCard extends preact.Component {
constructor(props) {
super(props);
this.dataRefreshToken = null;
this.state = {
user: {},
playlists: [],
Expand All @@ -93,7 +117,7 @@ class SpotifyCard extends preact.Component {
playingPlaylist: null,
authenticationRequired: true
};
this.scopes = ['user-read-private', 'user-read-email', 'playlist-read-private', 'user-read-birthdate', 'user-read-playback-state', 'user-modify-playback-state'];
this.scopes = ['playlist-read-private', 'user-read-playback-state', 'user-modify-playback-state'];
}

async componentDidMount() {
Expand All @@ -111,7 +135,6 @@ class SpotifyCard extends preact.Component {
if (userResp.error.status === 401) {
// Have a token but it is old
if (access_token && 0 + token_expires_ms - new Date().getTime() < 0) {
// console.log('Will do auth, has token but ut us old');
return this.authenticateSpotify();
} // no token - show login button

Expand All @@ -128,10 +151,6 @@ class SpotifyCard extends preact.Component {
});
}

this.setState({
authenticationRequired: false
});

if (hashParams.get('access_token')) {
const expires_in = hashParams.get('expires_in');
localStorage.setItem('access_token', access_token);
Expand All @@ -143,14 +162,30 @@ class SpotifyCard extends preact.Component {
}, '', newurl);
}

const playlists = await fetch('https://api.spotify.com/v1/me/playlists?limit=10', {
this.setState({
user: userResp,
authenticationRequired: false
});
await this.refreshPlayData();
this.dataRefreshToken = setInterval(async () => {
await this.refreshPlayData();
}, 5000);
}

componentWillUnmount() {
clearInterval(this.dataRefreshToken);
}

async refreshPlayData() {
const headers = {
Authorization: `Bearer ${localStorage.getItem('access_token')}`
};
const playlists = await fetch('https://api.spotify.com/v1/me/playlists?limit=' + this.props.limit, {
headers
}).then(r => r.json()).then(p => p.items);
const devices = await fetch('https://api.spotify.com/v1/me/player/devices', {
headers
}).then(r => r.json()).then(r => r.devices); // console.log('Response: playlists', playlists);
// console.log('Response: devices', devices);

}).then(r => r.json()).then(r => r.devices);
const currentPlayerRes = await fetch('https://api.spotify.com/v1/me/player', {
headers
});
Expand All @@ -164,14 +199,14 @@ class SpotifyCard extends preact.Component {
selectedDevice = currentPlayer.device;

if (currentPlayer.context && currentPlayer.context.external_urls) {
// console.log('Currently playing:', currentPlayer);
const currPlayingHref = currentPlayer.context.external_urls.spotify;
playingPlaylist = playlists.find(pl => currPlayingHref === pl.external_urls.spotify);
}
}
}

this.setState({
user: userResp,
playlists,
devices,
selectedDevice,
Expand All @@ -194,6 +229,7 @@ class SpotifyCard extends preact.Component {
} = this.state;

if (!selectedPlaylist || !selectedDevice) {
console.error('Will not play because there is no playlist or device selected');
return;
}

Expand All @@ -218,6 +254,27 @@ class SpotifyCard extends preact.Component {
this.playPlaylist();
}

onMediaPlayerSelect(device) {
this.setState({
selectedDevice: device
});
}

onChromecastDeviceSelect(device) {
const playlist = this.state.playingPlaylist ? this.state.playingPlaylist : this.state.playlists[0];

if (!playlist) {
console.error('Nothing to play, skipping starting chromecast device');
return;
} // console.log('Starting:', playlist.uri, ' on ', device.name);


this.props.hass.callService('spotcast', 'start', {
device_name: device.name,
uri: playlist.uri
});
}

getHighlighted(playlist) {
const {
selectedPlaylist
Expand All @@ -237,11 +294,10 @@ class SpotifyCard extends preact.Component {
render() {
const {
authenticationRequired,
user,
playlists,
devices,
selectedDevice
} = this.state; // console.log('SpotifyCard: playlists.length:', playlists.length, ' authenticationRequired:', authenticationRequired, ' devices.length', devices.length);
} = this.state;

if (authenticationRequired) {
return html`
Expand All @@ -259,10 +315,7 @@ class SpotifyCard extends preact.Component {
<${Header} />
<div class="playlists">
${playlists.map((playlist, idx) => {
const image = playlist.images[0] ? playlist.images[0].url : 'https://via.placeholder.com/150x150.png?text=No+image'; // if(!playlist.images[0]) {
// console.log('no image, click to expand the object to the right:', playlist.images);
// }
const image = playlist.images[0] ? playlist.images[0].url : 'https://via.placeholder.com/150x150.png?text=No+image';
return html`
<div
class="${`playlist ${this.getHighlighted(playlist)}`}"
Expand All @@ -280,9 +333,9 @@ class SpotifyCard extends preact.Component {
<${PlayerSelect}
devices=${devices}
selectedDevice=${selectedDevice}
onMediaplayerSelect=${device => this.setState({
selectedDevice: device
})}
hass=${this.props.hass}
onMediaplayerSelect=${device => this.onMediaPlayerSelect(device)}
onChromecastDeviceSelect=${device => this.onChromecastDeviceSelect(device)}
/>
</div>
</div>
Expand Down Expand Up @@ -454,6 +507,7 @@ class SpotifyCardWebComponent extends HTMLElement {
}

set hass(hass) {
// console.log('HASS:', hass);
if (!this.savedHass) {
this.savedHass = hass;
}
Expand Down Expand Up @@ -484,7 +538,7 @@ class SpotifyCardWebComponent extends HTMLElement {
this.shadow.appendChild(styleElement);
this.shadow.appendChild(mountPoint);
preact.render(html`
<${SpotifyCard} clientId=${this.config.client_id} />
<${SpotifyCard} clientId=${this.config.client_id} limit=${this.config.limit || 10} hass=${this.savedHass}/>
`, mountPoint);
}

Expand Down
Loading

0 comments on commit 7bcf6fb

Please sign in to comment.