Spotify Widget

OK, I used the code tag but “three backticks csv” did the trick :wink:

1 Like

Awesome, thank you so much!

I love this and am using it as a base with a few tweaks. The device and playlist backgrounds are hard coded and dont work out of dark mode - wondering if there is a way to detect if dark mode is on or off?

this is my version.

I have removed Black bagrounds from the devices and playlist
Adjusted the track info to be below image like it is in the spotify app

Also made it so on a wider screen the image and info are side by side, and narrower device the image and info are above and below.

See how you guys like this one.

uid: Spotify_widget_0.00
tags: []
props:
  parameters:
    - description: Title for the widget
      label: Static Title
      name: title
      required: false
      type: TEXT
    - context: item
      label: Select a 'Spotify Player Bridge' (SpotifyPlayerBridge)
      name: prefix
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Feb 4, 2021, 7:25:09 PM
component: f7-card
config:
  title: =props.title
  style:
    min-width: 270px
slots:
  default:
    - component: f7-row
      config:
        class: margin display-flex align-items-center
      slots:
        default:
          - component: f7-col
            config:
              width: 100
              xsmall: 30
              small: 50
              medium: 50
              large: 50
              xlarge: 50
            slots:
              default:
               
                - component: f7-row
                  config:
                    class: '=(items[props.prefix+"_AlbumImage"].state === "NULL")?"display-none" : "- margin-vertical - justify-content-center"'
                  slots:
                    default:
                      - component: oh-image
                        config:
                          item: =props.prefix+"_AlbumImage"
                          style:
                            width: 70%
          - component: f7-col
            config:
              
              width: 100
              xsmall: 30
              small: 50
              medium: 50
              large: 50
              xlarge: 50
            slots:
              default:                
              - component: f7-row
                config:
                  class: = (items[props.prefix+"_ActiveDeviceName"].state !== "NULL")?"display-none":"display-flex justify-content-center"
                slots:
                  default:
                    - component: Label
                      config:
                        text: This Spotify Bridge is Unavailable
              - component: f7-row
                config:
                  class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                slots:
                  default:
                    - component: Label
                      config:
                        text: =items[props.prefix+"_MediaArtist"].state || "-"
                        style:
                          white-space: nowrap
                          overflow: hidden
                          font-size: normal
                          font-weight: bold
                          font-style: italic
              - component: f7-row
                config:
                  class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                slots:
                  default:
                    - component: Label
                      config:
                        text: =items[props.prefix+"_MediaTitle"].state || "-"
                        style:
                          white-space: nowrap
                          overflow: hidden
                          font-size: normal
                          font-weight: bold
              - component: f7-row
                config:
                  class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                slots:
                  default:
                    - component: Label
                      config:
                        text: =items[props.prefix+"_AlbumName"].state || "-"
                        style:
                          white-space: nowrap
                          overflow: hidden
                          font-size: normal
                          font-style: italic

    - component: f7-row
      config:
        style:
          position: relative
          top: -10px
          height: 20px
        class:
          - justify-content-center
      slots:
        default:
          - component: f7-card
            config:
              noShadow: true
              class: margin display-flex align-items-center
              style:
                fontSize: 12px
                width: calc(100% - 80px)
            slots:
              default:
                - component: Label
                  config:
                    text: = items[props.prefix+"_TrackProgressmss"].state
                - component: f7-progressbar
                  config:
                    color: teal
                    progress: = 100 * items[props.prefix+"_TrackProgressms"].state / items[props.prefix+"_TrackDurationms"].state
                    style:
                      height: 10px
                    item: props.prefix+"_TrackProgressmss"
                - component: Label
                  config:
                    text: = items[props.prefix+"_TrackDurationmss"].state
    - component: f7-row
      config:
        class:
          - justify-content-space-around
          - display-flex
          - align-items-center
          - align-content-stretch
          - margin-top
      slots:
        default:
          - component: f7-icon
            config:
              f7: '=(props.prefix+"_ActiveDeviceShuffle") ? "shuffle" : ""'
              size: 20
              color: '=(items[props.prefix+"_ActiveDeviceShuffle"].state === "ON") ? "green" : ""'
              style:
                position: relative
                left: +7%
            slots:
              default:
                - component: oh-button
                  config:
                    action: command
                    actionItem: = props.prefix+"_ActiveDeviceShuffle"
                    actionCommand: '=(items[props.prefix+"_ActiveDeviceShuffle"].state !== "ON") ? "ON" : "OFF"'
                    style:
                      position: absolute
                      width: 100%
                      height: 100%
                      top: 0px
          - component: oh-player-item
            config:
              style:
                width: 150px
              item: =props.prefix+"_MediaControl"
              class:
                - display-flex
                - margin-
                - align-content-stretch
                - align-items-center
                - justify-content-space-around
          - component: f7-icon
            config:
              f7: '=(props.prefix+"_RepeatMode") ? (items[props.prefix+"_RepeatMode"].state === "context") ? "repeat" : (items[props.prefix+"_RepeatMode"].state === "track") ? "repeat_1" : "repeat" : ""'
              size: 20
              color: '=(items[props.prefix+"_RepeatMode"].state === "context") ? "green" : (items[props.prefix+"_RepeatMode"].state === "track") ? "green" : ""'
              style:
                position: relative
                left: -8%
            slots:
              default:
                - component: oh-button
                  config:
                    action: command
                    actionItem: = props.prefix+"_RepeatMode"
                    actionCommand: '=(items[props.prefix+"_RepeatMode"].state === "context") ? "track" : (items[props.prefix+"_RepeatMode"].state === "track") ? "off": "context"'
                    style:
                      position: absolute
                      width: 100%
                      height: 100%
                      top: 0px
    - component: f7-row
      config:
        class:
          - justify-content-space-around
          - display-flex
          - align-items-center
          - align-content-stretch
          - margin-top
      slots:
        default:
          - component: f7-card
            config:
              noShadow: true
              class: margin display-flex align-items-center
              style:
                height: 20px
                fontSize: 20px
                width: 100%
            slots:
              default:
                - component: f7-icon
                  config:
                    f7: speaker_3
                    class: margin-horizontal margin
                    size: 25
                - component: oh-slider
                  config:
                    label: true
                    style:
                      width: 75%
                      --f7-range-knob-color: rgba(122,122,122,0.8)
                      --f7-range-bar-size: 12px
                      --f7-range-bar-border-radius: 6px
                      --f7-range-knob-size: 16px
                      --f7-range-bar-bg-color: rgba(122,122,122,0.2)
                      --f7-range-bar-active-bg-color: linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,0.6))
                      --f7-range-knob-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
                    item: =props.prefix+"_Volume"
    - component: f7-row
      config:
        class: -justify-content-space-around -align-items-center -align-content-stretch
      slots:
        default:
          - component: f7-col
            config:
              noShadow: true
              class: resizable
              style:
                fontSize: 20px
                overflow: hidden
            slots:
              default:
                - component: oh-label-card
                  config:
                    icon: f7:hifispeaker
                    iconSize: 20
                    action: options
                    actionItem: =props.prefix+"_ActiveDevices"
                    item: =props.prefix+"_ActiveDevices"
                    style:
                      height: 100%
                    fontSize: small
          - component: f7-col
            config:
              noShadow: true
              class: resizable
              style:
                fontSize: 20px
                overflow: hidden
            slots:
              default:
                - component: oh-label-card
                  config:
                    icon: f7:music_note_list
                    iconSize: 20
                    action: options
                    actionItem: =props.prefix+"_Playlists"
                    item: =props.prefix+"_Playlists"
                    fontSize: small

4 Likes

Great widget !

I am trying to have it automatically list the “castable” devices when you clikc the “speaker aka output” button, but am not getting consistent results.

If a regular spotify client is casting to a device, the widget sees that and the output shows the cast name.

There are other cast desinations on the network and they do show up with a client, but in the widget, it shows only two - the one playing and a random hardwired one (a mac) that is not in OH3 as anything.

Any ideas on how to get this working would be appreciated.

For me I have a problem with the volume. The slider is not working properly. When I click on “100%” it is only changing to 10 or something like this. I saw, that i can move the slider out of the normal range and then it is working

This might be a general UI issue. I had very similar reports for one of my widgets. See this post here: OH3 Color/White Bulb Widget - #11 by tardismechanic

And I recognized that the devices for spotify are displayed via _ActiveDevices. The problem is that my google home minis seem to standby spotify when not used. So they are in status GONE.
This leads to the situation that I only see the current device and can not switch to another device. Is there any solution to display all spotify devices?

This might have been fixed by Wrong width of oh-slider component in oh-cell · Issue #967 · openhab/openhab-webui · GitHub.
This fix is included in 3.1.M3.

1 Like

Thanks for the widget, works great!

A possible developement proposal/question: there is a great widget for Habpanel, what do you think about an integration/translation to the OH3 UI? I would love to use/test/tweak it but it is way beyond my knowledge to create it.

I have tried to have a page with open.spotify.com as a web card, but it does not work, I guess Spotify is not allowing embedding the whole player. :frowning:

Have a great weekend!

@skibro - See this post as a great starting point for an openHAB 3 Main UI Widget; could likely be adapted to do what you want:

Thanks! I have checked that, the multiroom rule + the widget is amazing! I am more looking forward to have basic playlist selection + search functions within the MainUI, but I found a workaround: I have a button which opens the spotify web player in a new tab. Hope someone can found a smoother solution in the future :slight_smile:

I have just added a label card with action “external URL”. It opens Spotify Web player in an other chrome tab.

Thanks - after some searching I found the url to be used. Works great!

Thanks for posting your widgets. I mixed the widgets to my needs and want to share it.

spotify.items for convenience

Player         Spotify_TrackPlayer         "Player"                     {channel="spotify:player:user1:trackPlayer"}
Dimmer         Spotify_Volume              "Volume [%s]"                {channel="spotify:player:user1:deviceVolume"}
String         Spotify_Devices             "Active device [%s]"         {channel="spotify:player:user1:devices"}
Switch         Spotify_DeviceShuffle       "Shuffle mode"               {channel="spotify:player:user1:deviceShuffle"}
String         Spotify_TrackRepeat         "Repeat mode: [%s]"          {channel="spotify:player:user1:trackRepeat"}
String         Spotify_TrackProgress       "Track progress: [%s]"       {channel="spotify:player:user1:trackProgress"}
String         Spotify_TrackDuration       "Track duration: [%s]"       {channel="spotify:player:user1:trackDuration"}
Number:Time    Spotify_TrackProgressms     "Track progress: [%s]"       {channel="spotify:player:user1:trackProgressMs"}
Number:Time    Spotify_TrackDurationms     "Track duration: [%s]"       {channel="spotify:player:user1:trackDurationMs"}
String         Spotify_TrackName           "Track Name: [%s]"           {channel="spotify:player:user1:trackName"}
String         Spotify_AlbumName           "Album Name: [%s]"           {channel="spotify:player:user1:albumName"}
String         Spotify_ArtistName          "Artist Name: [%s]"          {channel="spotify:player:user1:artistName"}
String         Spotify_AlbumImageUrl       "Album Art Url"              {channel="spotify:player:user1:albumImageUrl"}
String         Spotify_Playlists           "Playlists [%s]"             {channel="spotify:player:user1:playlists"}
String         Spotify_PlayName            "Playlist [%s]"              {channel="spotify:player:user1:playlistName"}
String         Spotify_ActiveDeviceName    "Active Device Name [%s]"    {channel="spotify:player:user1:deviceName"}
String         Spotify_ActiveDevices       "Active Devices [%s]"        {channel="spotify:player:user1:devices"}
Switch         Spotify_TrackExplicit       "Track explicit [%s]"        {channel="spotify:player:user1:trackExplicit"}
String         Spotify_TrackUri            "Track uri [%s]"             {channel="spotify:player:user1:trackUri"}
String         Spotify_ArtistUri           "Artist uri [%s]"            {channel="spotify:player:user1:artistUri"}
String         Spotify_AlbumUri            "Album uri [%s]"             {channel="spotify:player:user1:albumUri"}
uid: Spotify_widget_0.03
tags: []
props:
  parameters:
    - default: Spotify
      description: Title for the widget
      label: Static Title
      name: title
      required: false
      type: TEXT
    - default: Spotify
      description: How all the Items associated with spotify start
      label: Item prefix
      name: prefix
      required: true
      type: TEXT
  parameterGroups: []
timestamp: Dec 28, 2021, 3:18:59 PM
component: f7-card
config:
  title: =props.title
  style:
    min-width: 270px
slots:
  default:
    - component: f7-row
      config:
        class: margin display-flex align-items-center
      slots:
        default:
          - component: f7-col
            config:
              width: 100
              xsmall: 30
              small: 50
              medium: 50
              large: 50
              xlarge: 50
            slots:
              default:
                - component: f7-row
                  config:
                    class: '=(items[props.prefix+"_AlbumImageUrl"].state === "NULL")?"display-none" : "- margin-vertical - justify-content-center"'
                  slots:
                    default:
                      - component: oh-image
                        config:
                          item: =props.prefix+"_AlbumImageUrl"
                          style:
                            width: 70%
          - component: f7-col
            config:
              width: 100
              xsmall: 30
              small: 50
              medium: 50
              large: 50
              xlarge: 50
            slots:
              default:
                - component: f7-row
                  config:
                    class: = (items[props.prefix+"_ActiveDeviceName"].state !== "NULL")?"display-none":"display-flex justify-content-center"
                  slots:
                    default:
                      - component: Label
                        config:
                          text: This Spotify Bridge is Unavailable
                - component: f7-row
                  config:
                    class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                  slots:
                    default:
                      - component: oh-link
                        config:
                          text: =items[props.prefix+"_TrackName"].state || "-"
                          action: url
                          actionUrl: =items[props.prefix+"_TrackUri"].state
                          color: teal
                          style:
                            white-space: nowrap
                            overflow: hidden
                            font-size: normal
                            font-weight: bold
                - component: f7-row
                  config:
                    class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                  slots:
                    default:
                      - component: Label
                        config:
                          text: =items[props.prefix+"_ArtistName"].state || "-"
                          style:
                            white-space: nowrap
                            overflow: hidden
                            font-size: normal
                - component: f7-row
                  config:
                    class: = (items[props.prefix+"_ActiveDeviceName"].state === "NULL")?"display-none":"display-flex justify-content-center"
                  slots:
                    default:
                      - component: Label
                        config:
                          text: =items[props.prefix+"_AlbumName"].state || "-"
                          style:
                            white-space: nowrap
                            overflow: hidden
                            font-size: normal
                            font-style: italic
                - component: f7-row
                  config:
                    class: = (items[props.prefix+"_TrackExplicit"].state != "ON")?"display-none":"display-flex justify-content-center"
                  slots:
                    default:
                      - component: f7-badge
                        slots:
                          default:
                            - component: Label
                              config:
                                text: E
    - component: f7-row
      config:
        style:
          position: relative
          top: -20px
          height: 20px
        class:
          - justify-content-center
      slots:
        default:
          - component: f7-card
            config:
              noShadow: true
              class: margin display-flex align-items-center
              style:
                fontSize: 12px
                width: calc(100% - 80px)
            slots:
              default:
                - component: Label
                  config:
                    text: = items[props.prefix+"_TrackProgress"].state
                    class: margin-right
                - component: f7-progressbar
                  config:
                    color: teal
                    progress: = 100 * items[props.prefix+"_TrackProgressms"].state / items[props.prefix+"_TrackDurationms"].state
                    style:
                      height: 10px
                    item: props.prefix+"_TrackProgress"
                - component: Label
                  config:
                    text: = items[props.prefix+"_TrackDuration"].state
                    class: margin-left
    - component: f7-row
      config:
        class:
          - justify-content-space-around
          - display-flex
          - align-items-center
          - align-content-stretch
          - margin-top
      slots:
        default:
          - component: f7-icon
            config:
              f7: '=(props.prefix+"_DeviceShuffle") ? "shuffle" : ""'
              size: 20
              color: '=(items[props.prefix+"_DeviceShuffle"].state === "ON") ? "teal" : ""'
              style:
                position: relative
                left: +7%
            slots:
              default:
                - component: oh-button
                  config:
                    action: command
                    actionItem: = props.prefix+"_DeviceShuffle"
                    actionCommand: '=(items[props.prefix+"_DeviceShuffle"].state !== "ON") ? "ON" : "OFF"'
                    style:
                      position: absolute
                      width: 100%
                      height: 100%
                      top: 0px
          - component: oh-player-item
            config:
              style:
                width: 150px
              item: =props.prefix+"_TrackPlayer"
              class:
                - display-flex
                - margin-
                - align-content-stretch
                - align-items-center
                - justify-content-space-around
          - component: f7-icon
            config:
              f7: '=(props.prefix+"_TrackRepeat") ? (items[props.prefix+"_TrackRepeat"].state === "context") ? "repeat" : (items[props.prefix+"_TrackRepeat"].state === "track") ? "repeat_1" : "repeat" : ""'
              size: 20
              color: '=(items[props.prefix+"_TrackRepeat"].state === "context") ? "teal" : (items[props.prefix+"_TrackRepeat"].state === "track") ? "teal" : ""'
              style:
                position: relative
                left: -8%
            slots:
              default:
                - component: oh-button
                  config:
                    action: command
                    actionItem: = props.prefix+"_TrackRepeat"
                    actionCommand: '=(items[props.prefix+"_TrackRepeat"].state === "context") ? "track" : (items[props.prefix+"_TrackRepeat"].state === "track") ? "off": "context"'
                    style:
                      position: absolute
                      width: 100%
                      height: 100%
                      top: 0px
    - component: f7-row
      config:
        class:
          - justify-content-space-around
          - display-flex
          - align-items-center
          - align-content-stretch
          - margin-top
      slots:
        default:
          - component: f7-card
            config:
              noShadow: true
              class: margin display-flex align-items-center
              style:
                height: 20px
                fontSize: 20px
                width: 100%
            slots:
              default:
                - component: f7-icon
                  config:
                    f7: speaker_3
                    class: margin-horizontal margin
                    size: 25
                - component: oh-slider
                  config:
                    label: true
                    class: margin
                    min: 0
                    max: 100
                    step: 10
                    unit: "%"
                    style:
                      width: 70%
                      --f7-range-knob-color: rgba(122,122,122,1)
                      --f7-range-bar-size: 12px
                      --f7-range-bar-border-radius: 6px
                      --f7-range-knob-size: 16px
                      --f7-range-bar-bg-color: rgba(122,122,122,0.2)
                      --f7-range-bar-active-bg-color: linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,0.6))
                      --f7-range-knob-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
                    item: =props.prefix+"_Volume"
    - component: f7-row
      config:
        class:
          - justify-content-space-around
          - align-items-center
          - align-content-stretch
          - margin
      slots:
        default:
          - component: f7-col
            config:
              noShadow: true
              class: resizable
              style:
                fontSize: 20px
                overflow: hidden
            slots:
              default:
                - component: oh-label-card
                  config:
                    icon: f7:hifispeaker
                    iconSize: 20
                    action: options
                    actionItem: =props.prefix+"_ActiveDevices"
                    item: =props.prefix+"_ActiveDevices"
                    style:
                      height: 100%
                    fontSize: small
          - component: f7-col
            config:
              noShadow: true
              class: resizable
              style:
                fontSize: 20px
                overflow: hidden
            slots:
              default:
                - component: oh-label-card
                  config:
                    icon: f7:music_note_list
                    iconSize: 20
                    action: options
                    actionItem: =props.prefix+"_Playlists"
                    item: =props.prefix+"_Playlists"
                    fontSize: small

I noticed some flaws:

  • when playing another playlist than listed a spotify:playlist: string is shown in the bottom right select
  • does not look nice if some props are missing (some NULL here and there)
2 Likes

May I ask a stupid question, got the widget working (ok a small problem with the Album Image because I am still running 3.1.0 which has the image where newer versions the reference to the URL, maybe this remark will trigger other people with the same problem) back to the question :smiley:

The question is: if I look in the HABpanel I do not see this newly made custom widget, what did I not do yet?

HABPanel is another UI. This widget is for the Layout Pages. Pages - Introduction | openHAB

@No3x clear, thanks for your swift reply.

Hello,
thank you all for that nice widget!
I have one issue:

When no spotify device is connected to and there is no music track played OR while playing a podcast track, the picture of the item _AlbumImageUrl shows a “broken picture” icon, because there is nothing found what could be shown or as default. Is there a possibility to show a default or alternative picture via url path? e.g. a local saved picture?

slots:
  default:
    - component: oh-image
      config:
        item: =props.prefix+"_AlbumImageUrl"

Regards.

Newer openhab cores are now auto renaming the items to have under scores so the above would be…

=props.prefix+“_Album_Image_Url”

The first post seems to have a newer widget that means you need to specify each item. Just a heads up to anyone using old widget code and then re-creates the items with the newer naming.