Closed matiasoberg closed 8 months ago
I really wanted a good way to see the current queue but still struggle with Jinja2 and templates.
Came up with this code as a custom card that leverages on the awesomeness of what's already provide by @hokiebrian. Ugly workaround but does the trick.
Create a .js file in /config/www/ Name it something appropriate. I called mine spotify-queue-card.js Reference it as a Lovelace reference. Restart and create the card manually.
class SpotifyQueueCard extends HTMLElement {
set hass(hass) {
if (!this.content) {
this.innerHTML = `
<ha-card>
<div class="now-playing"></div>
<div class="up-next"></div>
</ha-card>
`;
this.nowPlaying = this.querySelector(".now-playing");
this.upNext = this.querySelector(".up-next");
}
const nowPlayingState = hass.states[this.config.now_playing_sensor];
const queueState = hass.states[this.config.queue_sensor];
// Now playing section
if (nowPlayingState && nowPlayingState.attributes.spotify) {
const nowPlaying = nowPlayingState.attributes.spotify;
const trackName = nowPlaying.spotify_track_name || 'Unknown Track';
const artistName = nowPlaying.spotify_artist_name || 'Unknown Artist';
const artistImage = nowPlaying.spotify_artist_img || '';
this.nowPlaying.innerHTML = `
<div>
<strong>Now playing:</strong><br>
<ul style="margin-top: 10px;">
<li style="display: flex; align-items: center; margin-bottom: 5px;">
<span style="width: 30px; text-align: right; margin-right: 10px;">1</span>
<img src="${artistImage}" height="50px" style="margin-right: 10px;">
<span>${trackName}<br>${artistName}</span>
</li>
</ul>
</div>
`;
} else {
this.nowPlaying.innerHTML = '<strong>Now playing:</strong> No current track';
}
// Up next section
if (queueState && queueState.attributes && queueState.attributes.queue) {
const queue = queueState.attributes.queue;
const playlistName = nowPlayingState && nowPlayingState.attributes.spotify && nowPlayingState.attributes.spotify.spotify_playlist && nowPlayingState.attributes.spotify.spotify_playlist.name ? nowPlayingState.attributes.spotify.spotify_playlist.name : 'Unknown Playlist';
this.upNext.innerHTML = `
<strong>Next from: ${playlistName}</strong>
<ul style="margin-top: 10px;">
${queue.map((track, index) => `
<li style="display: flex; align-items: center; margin-bottom: 5px;">
<span style="width: 30px; text-align: right; margin-right: 10px;">${index + 2}</span>
<img src="${track.image}" height="50px" style="margin-right: 10px;">
<span>${track.trackname}<br>${track.trackartist}</span>
</li>`
).join('')}
</ul>
`;
} else {
this.upNext.innerHTML = '<strong>Next from:</strong> No tracks in queue';
}
this.applyStyles();
}
applyStyles() {
this.style.cssText = `
ha-card {
padding: 16px;
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.now-playing, .up-next {
margin-bottom: 16px;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: flex;
align-items: center;
margin-bottom: 10px;
}
img {
border-radius: var(--ha-card-border-radius);
}
span {
margin-right: 10px;
}
`;
}
setConfig(config) {
if (!config.now_playing_sensor || !config.queue_sensor) {
throw new Error("You need to define both now_playing_sensor and queue_sensor");
}
this.config = config;
}
getCardSize() {
return 4;
}
}
customElements.define("spotify-queue-card", SpotifyQueueCard);
Here's what I use for my queue. It will show the song info, album art, and have tap actions to add to library, play, etc.
- type: custom:auto-entities
card:
square: false
type: grid
columns: 2
card_param: cards
filter:
template: |-
{%-
for item in state_attr('sensor.spotify_extras', 'queue') -%}
{{
{
'type': 'custom:button-card',
'layout': 'icon_name_state2nd',
'template': ['recentqueue'],
'variables': {'itemName':item.trackname, 'itemURI':item.trackuri, 'itemID':item.trackuri, 'itemSaved':item.saved},
'label': '',
'name': item.trackartist + '<br>' + item.trackname,
'entity_picture': item.image,
'tap_action': {'service_data':{'media_content_type':'music','media_content_id': item.trackuri} },
'double_tap_action': {'service_data':{'libraryinfo':'track','track_id': item.trackuri,'origin':'RecentQueue'} },
'hold_action': {'service_data':{'libraryinfo':'untrack','track_id': item.trackuri,'origin':'RecentQueue'} },
}
}},
{%- endfor %}
The referenced template is:
recentqueue:
variables:
itemPlayed: none
label: |
[[[
const timeago = (new Date() - new
Date(variables.itemPlayed)) / 1000;
const minutes = Math.floor((timeago / 60));
const formattedMinutes = String(minutes).padStart(0, '0');
return `${formattedMinutes} minutes ago`;
]]]
show_label: true
show_name: true
aspect_ratio: 4.5/1
show_entity_picture: true
styles:
card:
- font-size: .6em
- padding: 0px
- border: 0px
entity_picture:
- scale: 1
- padding: 0px
custom_fields:
logo:
- position: absolute
- top: 35%
- left: 31%
- color: firebrick
- opacity: |
[[[
if (variables.itemSaved) return "90%";
else return "10%";
]]]
custom_fields:
logo: |
[[[
return `<img src=/local/spotify-icon.png width=15 height=15>`
]]]
tap_action:
action: call-service
haptic: light
service: media_player.play_media
confirmation:
text: '[[[ return `Play: ${variables.itemName}` ]]]'
service_data:
media_content_type: playlist
entity_id: media_player.spotify_home
double_tap_action:
action: call-service
haptic: light
service: script.song_library_tools
Brilliant!
Thanks a lot for sharing @hokiebrian !
Keep up the great work :)
Got it to work. One final piece of the puzzle I'm missing is the "script.song_library_tools" that you reference in the template. Care to share that one too?
Thanks!
Here's the YAML for the script. It basically takes the argument for what you're trying to do (add track, add album, add artists, etc) and then triggers a refresh of the song data to reflect the updated library status. Worth noting - the mode is queued with max of 10. That way you can blast through adding multiple tracks, etc. The delay step ended up needing to be added because there were some cases when the refresh was too fast and the library status wasn't reflected properly upon refresh.
alias: Song Library Tools
fields:
libraryinfo:
name: Library Function
description: Type of function
required: true
example: track
selector:
text: null
playlist_id:
name: Playlist URI
description: Playlist URI
required: false
example: spotify:playlist:ABC123
selector:
text: null
album_id:
name: Album ID
description: Album ID
required: false
example: ABC123
selector:
text: null
track_id:
name: Track ID
description: Track ID
required: false
example: ABC123
selector:
text: null
update_card:
name: Update Card?
description: Refresh origin card?
required: false
example: true
selector:
boolean: null
origin:
name: Search Origin
description: Search Origin
required: false
example: Artist Profile
selector:
select:
options:
- label: Artist Profile
value: Artist Profile
- label: General Search
value: General Search
sequence:
- choose:
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "playlist" }}
sequence:
- service: spotify_plus.spotify_follow_playlist
data:
playlist_id: "{{ playlist_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "album" }}
sequence:
- service: spotify_plus.spotify_follow_album
data:
album_id: "{{ album_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "track" }}
sequence:
- service: spotify_plus.spotify_follow_track
data:
track_id: "{{ track_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "artists" }}
sequence:
- service: spotify_plus.spotify_follow_artist
data:
artist_id: "{{ artist_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "unplaylist" }}
sequence:
- service: spotify_plus.spotify_unfollow_playlist
data:
playlist_id: "{{ playlist_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "unalbum" }}
sequence:
- service: spotify_plus.spotify_unfollow_album
data:
album_id: "{{ album_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "untrack" }}
sequence:
- service: spotify_plus.spotify_unfollow_track
data:
track_id: "{{ track_id }}"
- conditions:
- condition: template
value_template: |
{{ libraryinfo == "unartists" }}
sequence:
- service: spotify_plus.spotify_unfollow_artist
data:
artist_id: "{{ artist_id }}"
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: 250
- if:
- condition: template
value_template: "{{ update_card }}"
then:
- service: spotify_plus.spotify_search
data:
search_term: "{{ states('input_text.spotify_search')}}"
search_type: "{{ origin }}"
- if:
- condition: template
value_template: "{{ is_state('media_player.spotify_home', ['playing', 'paused']) }}"
then:
- service: spotify_plus.get_song_data
data: {}
- service: spotify_plus.spotify_extras
data: {}
mode: queued
max: 10
You are a true gentleman sir. Thank you very much for sharing your vast knowledge on this topic. I'm learning a lot by just going through your code and adopting it to my own needs. Thanks again and Happy New Year :)
Hi,
First of all.
Fabulous job on this HA Spotify Integration. @hokiebrian
Was contemplating to dive into the Spotify API myself but you have already done all the needed job. It's a shame it doesn't get more exposure in the HA community.
I was about to create an issue to say exactly the same thing so better hijack this closed thread instead : this integration is incredibly powerful and well-crafted, thanks a lot @hokiebrian this should have way more exposure.
Currently playing with the possibilities, I second the idea of publishing some ideas of advanced scripts like you did in this thread.
First of all.
Fabulous job on this HA Spotify Integration. @hokiebrian
Was contemplating to dive into the Spotify API myself but you have already done all the needed job. It's a shame it doesn't get more exposure in the HA community.
Your examples looks great and I think it would be very valuable if you could share the code on how you have set up those examples. Explanations are great already but getting a bit of push in the right direction would save a lot of us a lot of time I recon.
Cheers!