Now-playing card for the SoundCloud binding: the background follows the cover colours, long titles scroll, a search panel opens from the magnifier icon (tap a result to play it), the progress bar is seekable and the selected Chromecast is shown as a chip. The texts follow your openHAB language (German or English).
Requires the SoundCloud binding v1.6.0 or newer. Set the widget properties to the items linked to the binding channels (player#title, player#playback-state, player#track-id, search#query, search#results, player#chromecast-target, optionally player#volume, player#mute, β¦). Full item list and property table: see the binding post.
Screenshots
Changelog
Version 1.1
- added: texts follow the openHAB language (German / English)
- changed: English property labels; removed obsolete playlist properties
Version 1.0
- initial release
Resources
uid: soundcloud_nowplaying
tags:
- soundcloud
props:
parameterGroups:
- name: player
label: Player
- name: search
label: Search
- name: cast
label: Chromecast
parameters:
- name: titleItem
label: Title
description: String item β player#title
type: TEXT
context: item
groupName: player
required: true
- name: artistItem
label: Artist
description: String item β player#artist
type: TEXT
context: item
groupName: player
- name: artworkItem
label: Cover URL
description: String item β player#artwork-url
type: TEXT
context: item
groupName: player
- name: stateItem
label: Playback state
description: String item β player#playback-state
type: TEXT
context: item
groupName: player
required: true
- name: trackIdItem
label: Track ID
description: String item β player#track-id
type: TEXT
context: item
groupName: player
required: true
- name: streamUrlItem
label: Stream URL
description: String item β player#stream-url
type: TEXT
context: item
groupName: player
- name: elapsedItem
label: Elapsed time
description: Number item β player#elapsed-time
type: TEXT
context: item
groupName: player
- name: durationItem
label: Duration
description: Number item β player#duration
type: TEXT
context: item
groupName: player
- name: seekPctItem
label: Seek (%)
description: "Optional: Number item 0β100 to seek with the progress bar (needs the seek rule from the binding repo)"
type: TEXT
context: item
groupName: player
- name: queueIndexItem
label: Queue position
description: Number item β player#queue-index
type: TEXT
context: item
groupName: player
- name: queueSizeItem
label: Queue size
description: Number item β player#queue-size
type: TEXT
context: item
groupName: player
- name: shuffleItem
label: Shuffle
description: Switch item β player#shuffle
type: TEXT
context: item
groupName: player
- name: repeatItem
label: Repeat
description: String item β player#repeat (OFF / PLAYLIST / TRACK)
type: TEXT
context: item
groupName: player
- name: queryItem
label: Search query
description: String item β search#query
type: TEXT
context: item
groupName: search
required: true
- name: resultsItem
label: Search results
description: String item β search#results
type: TEXT
context: item
groupName: search
required: true
- name: targetItem
label: Chromecast target
description: String item β player#chromecast-target
type: TEXT
context: item
groupName: cast
required: true
- name: chromecastTag
label: Chromecast tag
description: Tag of your Chromecast playuri items (default Chromecast)
type: TEXT
default: Chromecast
groupName: cast
- name: volumeItem
label: Volume
description: Dimmer item β player#volume
type: TEXT
context: item
groupName: cast
- name: muteItem
label: Mute
description: Switch item β player#mute
type: TEXT
context: item
groupName: cast
component: f7-card
config:
style:
position: relative
overflow: clip
border-radius: 18px
margin: 0
max-width: 960px
margin-left: auto
margin-right: auto
background: "#222"
color: white
stylesheet: >
@keyframes sc-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.sc-mq { overflow: hidden; white-space: nowrap; width: 100%; }
.sc-mq-run { display: inline-flex; animation-name: sc-marquee; animation-timing-function: linear; animation-iteration-count: infinite; }
.sc-mq-run span { padding-right: 5em; }
.sc-mq-static { display: block; text-align: center; overflow: hidden; text-overflow: ellipsis; }
.sc-ctrl { display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; border-radius: 50%; color: white !important; }
.sc-ctrl:hover { background: rgba(255,255,255,0.12); }
.sc-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 10px; background: rgba(255,255,255,0.14); border: 1px solid rgba(255,255,255,0.25); color: white !important; font-size: 13px; }
.sc-chip.sc-sel { background: rgba(255,255,255,0.85); color: #222 !important; }
.sc-row { display: flex !important; align-items: center; gap: 12px; padding: 6px 4px; border-radius: 8px; color: white !important; text-align: left; }
.sc-row:hover { background: rgba(255,255,255,0.12); }
.sc-search input { width: 100%; color: white !important; caret-color: white; background: rgba(255,255,255,0.12) !important; border: 1px solid rgba(255,255,255,0.35) !important; border-radius: 10px; padding: 8px 12px; font-size: 15px; outline: none; }
.sc-search input::placeholder { color: rgba(255,255,255,0.6); }
.sc-seek { --f7-range-bar-height: 4px; --f7-range-knob-size: 12px; --f7-range-bar-active-bg-color: white; --f7-range-bar-bg-color: rgba(255,255,255,0.25); --f7-range-knob-color: white; }
slots:
default:
# Background: blurred cover
- component: div
config:
style:
position: absolute
inset: -80px
background-image: "=props.artworkItem && ('' + @@props.artworkItem).indexOf('http') === 0 ? 'url(' + @@props.artworkItem + ')' : 'none'"
background-size: cover
background-position: center
filter: blur(70px) saturate(1.5)
transform: scale(1.3)
- component: div
config:
style:
position: absolute
inset: 0
background: linear-gradient(180deg, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.30) 55%, rgba(0,0,0,0.55) 100%)
- component: div
config:
style:
position: relative
z-index: 1
display: flex
flex-direction: column
align-items: center
padding: 16px 24px 20px
gap: 10px
text-shadow: 0 1px 6px rgba(0,0,0,0.45)
slots:
default:
# Header
- component: div
config:
style:
display: flex
width: 100%
justify-content: space-between
align-items: center
slots:
default:
- component: div
config:
content: "=(@@props.stateItem === 'PLAYING' ? (dayjs.locale().indexOf('de') === 0 ? 'β spielt' : 'β playing') : (@@props.stateItem === 'PAUSED' ? (dayjs.locale().indexOf('de') === 0 ? 'ββ pausiert' : 'ββ paused') : (dayjs.locale().indexOf('de') === 0 ? 'β gestoppt' : 'β stopped'))) + (props.queueSizeItem && Number(@@props.queueSizeItem) > 1 ? ' Β· ' + Number(@@props.queueIndexItem) + ' / ' + Number(@@props.queueSizeItem) : '')"
style:
font-size: 13px
opacity: 0.85
padding: 4px 10px
border-radius: 8px
background: rgba(0,0,0,0.25)
white-space: pre
- component: div
config:
style:
display: flex
gap: 4px
slots:
default:
- component: oh-link
config:
class:
- sc-ctrl
iconF7: "=vars.showSearch ? 'xmark' : 'search'"
iconSize: 22
action: variable
actionVariable: showSearch
actionVariableValue: =!vars.showSearch
- component: oh-link
config:
class:
- sc-ctrl
iconF7: stop_fill
iconSize: 20
action: command
actionItem: =props.stateItem
actionCommand: STOP
# Search
- component: div
config:
visible: =vars.showSearch === true
style:
width: 100%
background: rgba(0,0,0,0.35)
border-radius: 14px
padding: 10px
box-sizing: border-box
slots:
default:
- component: div
config:
style:
display: flex
align-items: center
gap: 6px
slots:
default:
- component: oh-input
config:
class:
- sc-search
item: =props.queryItem
type: text
placeholder: "=dayjs.locale().indexOf('de') === 0 ? 'SoundCloud durchsuchen β¦' : 'Search SoundCloud β¦'"
sendButton: true
clearButton: true
style:
flex: 1
- component: div
config:
visible: "=('' + @@props.resultsItem) === '[]'"
content: "=dayjs.locale().indexOf('de') === 0 ? 'Keine Treffer' : 'No results'"
style:
padding: 10px 4px
opacity: 0.8
- component: div
config:
style:
max-height: 320px
overflow-y: auto
margin-top: 6px
slots:
default:
- component: oh-repeater
config:
for: r
sourceType: array
in: "=('' + @@props.resultsItem).indexOf('[') === 0 ? JSON.parse(@@props.resultsItem) : []"
fragment: true
slots:
default:
- component: oh-link
config:
class:
- sc-row
action: command
actionItem: =props.trackIdItem
actionCommand: ='' + loop.r.id
slots:
default:
- component: div
config:
style:
width: 44px
height: 44px
flex: none
border-radius: 6px
background-color: rgba(255,255,255,0.15)
background-image: "=loop.r.artwork ? 'url(' + loop.r.artwork + ')' : 'none'"
background-size: cover
background-position: center
- component: div
config:
style:
flex: 1
min-width: 0
slots:
default:
- component: div
config:
content: =loop.r.title
style:
font-weight: 600
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
- component: div
config:
content: "=(loop.r.artist || '') + (loop.r.duration ? ' Β· ' + Math.floor(loop.r.duration / 60) + ' Min' : '')"
style:
font-size: 12px
opacity: 0.75
white-space: pre
overflow: hidden
text-overflow: ellipsis
- component: f7-icon
config:
f7: play_circle
size: 26
# Cover
- component: div
config:
style:
width: min(400px, 75vw)
aspect-ratio: 1 / 1
border-radius: 12px
box-shadow: 0 12px 40px rgba(0,0,0,0.45)
background-color: rgba(255,255,255,0.08)
background-image: "=props.artworkItem && ('' + @@props.artworkItem).indexOf('http') === 0 ? 'url(' + @@props.artworkItem + ')' : 'none'"
background-size: cover
background-position: center
margin: 6px 0 10px
# Title (scrolls when long)
- component: div
config:
class:
- sc-mq
style:
font-size: 28px
font-weight: 700
slots:
default:
- component: div
config:
class: "=('' + @@props.titleItem).length > 34 ? 'sc-mq-run' : 'sc-mq-static'"
style:
animation-duration: "=(('' + @@props.titleItem).length * 0.3) + 's'"
slots:
default:
- component: span
config:
content: "=['NULL', 'UNDEF', ''].includes('' + @@props.titleItem) ? (dayjs.locale().indexOf('de') === 0 ? 'Nichts wird abgespielt' : 'Nothing playing') : @@props.titleItem"
- component: span
config:
visible: "=('' + @@props.titleItem).length > 34"
content: =@@props.titleItem
# Artist
- component: div
config:
visible: "=props.artistItem && !['NULL', 'UNDEF', ''].includes('' + @@props.artistItem)"
content: =@@props.artistItem
style:
font-size: 20px
opacity: 0.8
text-align: center
# Progress
- component: div
config:
visible: =!!props.durationItem
style:
width: 100%
margin-top: 14px
slots:
default:
- component: oh-slider
config:
visible: =!!props.seekPctItem
class:
- sc-seek
item: =props.seekPctItem
min: 0
max: 100
step: 1
releaseOnly: true
style:
width: 100%
- component: div
config:
visible: =!props.seekPctItem
style:
height: 4px
width: 100%
border-radius: 2px
background: rgba(255,255,255,0.25)
overflow: hidden
slots:
default:
- component: div
config:
style:
height: 100%
background: white
transition: width 1s linear
width: "=Math.min(100, (Number(@@props.elapsedItem) || 0) / (Number(@@props.durationItem) || 1) * 100) + '%'"
- component: div
config:
style:
display: flex
justify-content: space-between
font-size: 12px
opacity: 0.8
margin-top: 6px
slots:
default:
- component: span
config:
content: "=(Number(@@props.elapsedItem) >= 3600 ? Math.floor(Number(@@props.elapsedItem) / 3600) + ':' : '') + ('0' + Math.floor((Number(@@props.elapsedItem) || 0) % 3600 / 60)).slice(-2) + ':' + ('0' + Math.floor((Number(@@props.elapsedItem) || 0) % 60)).slice(-2)"
- component: span
config:
content: "=(Number(@@props.durationItem) >= 3600 ? Math.floor(Number(@@props.durationItem) / 3600) + ':' : '') + ('0' + Math.floor((Number(@@props.durationItem) || 0) % 3600 / 60)).slice(-2) + ':' + ('0' + Math.floor((Number(@@props.durationItem) || 0) % 60)).slice(-2)"
# Controls
- component: div
config:
style:
display: flex
width: 100%
justify-content: space-around
align-items: center
margin-top: 4px
slots:
default:
- component: oh-link
config:
class:
- sc-ctrl
iconF7: shuffle
iconSize: 22
action: toggle
actionItem: =props.shuffleItem
actionCommand: "ON"
actionCommandAlt: "OFF"
style:
opacity: "=props.shuffleItem && @@props.shuffleItem === 'ON' ? 1 : 0.55"
- component: oh-link
config:
class:
- sc-ctrl
iconF7: backward_end
iconSize: 26
action: command
actionItem: =props.stateItem
actionCommand: PREVIOUS
# Play / Pause (the binding controls the Chromecast)
- component: oh-link
config:
iconF7: "=@@props.stateItem === 'PLAYING' ? 'pause_fill' : 'play_fill'"
iconSize: 30
action: command
actionItem: =props.stateItem
actionCommand: "=@@props.stateItem === 'PLAYING' ? 'PAUSE' : 'PLAY'"
style:
display: flex
align-items: center
justify-content: center
width: 60px
height: 60px
border-radius: 50%
background: rgba(255,255,255,0.92)
color: "#222"
box-shadow: 0 4px 16px rgba(0,0,0,0.3)
- component: oh-link
config:
class:
- sc-ctrl
iconF7: forward_end
iconSize: 26
action: command
actionItem: =props.stateItem
actionCommand: NEXT
- component: oh-link
config:
class:
- sc-ctrl
iconF7: "=props.repeatItem && @@props.repeatItem === 'TRACK' ? 'repeat_1' : 'repeat'"
iconSize: 22
action: command
actionItem: =props.repeatItem
actionCommand: "=@@props.repeatItem === 'PLAYLIST' ? 'TRACK' : (@@props.repeatItem === 'TRACK' ? 'OFF' : 'PLAYLIST')"
style:
opacity: "=props.repeatItem && ['PLAYLIST', 'TRACK'].includes(@@props.repeatItem) ? 1 : 0.55"
# Volume (selected Chromecast via player#volume / player#mute)
- component: div
config:
style:
display: flex
width: 100%
align-items: center
gap: 10px
slots:
default:
- component: oh-link
config:
class:
- sc-ctrl
iconF7: "=@@props.muteItem === 'ON' ? 'speaker_slash' : 'speaker_2'"
iconSize: 20
action: toggle
actionItem: =props.muteItem
actionCommand: "ON"
actionCommandAlt: "OFF"
- component: oh-slider
config:
item: =props.volumeItem
min: 0
max: 100
step: 1
releaseOnly: true
style:
flex: 1
--f7-range-bar-active-bg-color: white
--f7-range-bar-bg-color: rgba(255,255,255,0.3)
--f7-range-knob-color: white
- component: span
config:
content: "=Number(@@props.volumeItem) >= 0 ? Math.round(Number(@@props.volumeItem)) : '-'"
style:
width: 30px
text-align: right
font-size: 13px
# Selected Chromecast (tap to open the device list)
- component: oh-repeater
config:
for: c
sourceType: itemsWithTags
itemTags: =props.chromecastTag || 'Chromecast'
filter: loop.c.name === @@props.targetItem
fragment: true
slots:
default:
- component: oh-link
config:
class:
- sc-chip
iconF7: "=vars.showCast ? 'chevron_down' : 'tv_music_note'"
iconSize: 16
text: =loop.c.label
action: variable
actionVariable: showCast
actionVariableValue: =!vars.showCast
- component: oh-link
config:
visible: "=['NULL', 'UNDEF', ''].includes('' + @@props.targetItem)"
class:
- sc-chip
iconF7: tv_music_note
iconSize: 16
text: "=dayjs.locale().indexOf('de') === 0 ? 'Chromecast wΓ€hlen' : 'Select Chromecast'"
action: variable
actionVariable: showCast
actionVariableValue: =!vars.showCast
- component: div
config:
visible: =vars.showCast === true
style:
display: flex
flex-wrap: wrap
justify-content: center
gap: 8px
padding-top: 6px
slots:
default:
- component: oh-repeater
config:
for: c
sourceType: itemsWithTags
itemTags: =props.chromecastTag || 'Chromecast'
fragment: true
slots:
default:
- component: oh-link
config:
class: "=loop.c.name === @@props.targetItem ? 'sc-chip sc-sel' : 'sc-chip'"
text: =loop.c.label
action: command
actionItem: =props.targetItem
actionCommand: =loop.c.name

