jishi / node-sonos-discovery

Simplified framework for Sonos built on node.js
MIT License
146 stars 75 forks source link

How to find albumart from metadata #56

Closed jishi closed 8 years ago

jishi commented 8 years ago

@ErwinvanderZwart I created a separate issue for this discussion, so we don't notify the other users in the other thread.

Getting album art for streaming services is actually the same principle as for local media, you would receive a relative URL with the metadata that you can invoke against any of your players. See the CurrentMetaData DIDL:

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
    <item id="-1" parentID="-1" restricted="true">
        <res protocolInfo="sonos.com-spotify:*:audio/x-spotify:*" duration="0:05:18">x-sonos-spotify:spotify%3atrack%3a5qAFqkXoQd2RfjZ2j1ay0w?sid=9&amp;flags=8224&amp;sn=9</res>
        <r:streamContent></r:streamContent>
        <r:radioShowMd></r:radioShowMd>
        <upnp:albumArtURI>/getaa?s=1&amp;u=x-sonos-spotify%3aspotify%253atrack%253a5qAFqkXoQd2RfjZ2j1ay0w%3fsid%3d9%26flags%3d8224%26sn%3d9</upnp:albumArtURI>
        <dc:title>Intermezzo No. 3 in C-sharp minor, Op. 117 - Andante con moto</dc:title>
        <upnp:class>object.item.audioItem.musicTrack</upnp:class>
        <dc:creator>Johannes Brahms</dc:creator>
        <upnp:album>Glenn Gould plays Brahms: 4 Ballades op. 10; 2 Rhapsodies op. 79; 10 Intermezzi</upnp:album>
    </item>
</DIDL-Lite>

As you can see, the upnp:albumArtURI tag contains a relative url that you can append to the player base url, which would be http://player_ip:1400/

This is the data that is evented, you are talking about TrackMetaData, are you polling that data? It might differ slightly.

jishi commented 8 years ago

Some services will return a full resolution image, others will just give you a 150x150 image. In order to get the full resolution, you would need to resort to the SMAPI that Sonos provided, but that will also require authentication. I resorted to direct integration against some popular services and their open APIs in order to get a full resolution image.

ErwinvanderZwart commented 8 years ago

@jishi

Hi Jishi,

Thank you for your comment.

Yes we are polling data from a controller running Linux and LUA scripting. From LUA we run a script that uses LUA socket and we can do anything with UPNP.

We get albumart from UPNP when playing local data or from iPhone, but the hard part is getting albumart working from streaming media.

How do you request the CurrentMetaData DIDL response? I don't see this field with UPNP device spy.

For example when i use GetMediaInfo i get:

CurrentURI: x-sonosapi-stream:s47309?sid=254&flags=8224&sn=0

CurrentURIMetaData: dc:titleNPO SterrenNL/dc:titleupnp:classobject.item.audioItem.audioBroadcast/upnp:classSARINCON65031

When i use GetPositionInfo i get:

TrackURI: aac://icecast.omroep.nl/radio2-sterrennl-aac

TrackMetaData: aac://icecast.omroep.nl/radio2-sterrennl-aac/r:streamContentdc:titleradio2-sterrennl-aac/dc:titleupnp:classobject.item/upnp:class

As you can see none of them holds the AlbumArtURI field ...

I don't want to use API keys or any other credentials to keep the scripts simple to include (plug and play) so i'm looking for a way to fetch the album art from a source that is working for longer period. This to avoid having lots of questions from integrators when it stops working as we do not deliver services or updates / maintainance. The Sonos is just a free nice add-on feature for the controller.

I prefer to use SMAPI because this will excist as long as SONOS excists (;

The problem i have is that some services like tune-in don't gives a albumArtURI but only TrackURI and Track metadata. I get radio station, current artist but no ART url / uri.

What services / open APIs do you use to get the artwork in that case? (without API key)

Thanks for your help so far.

Best regards,

Erwin van der Zwart Schneider Electric Netherlands

jishi commented 8 years ago

Hi, I'm not using anything special to get the album art, I use the one provided with the event data that I receive when the playback starts. Are you saying that my library has correct album art but it isn't included in the GetMediaInfo call? In that case, it is a difference between evented data and what you retrieve when polling for data.

ErwinvanderZwart commented 8 years ago

@jishi Hi Jimmy,

I don't know.. I'm just looking for a way to resolve my issue and somehow I think you already use the correct service i miss / overloop. (:

Maybe i do something wrong. All I know is that I can get albumart from AVtransport service with GetPositionInfo call when playing local media.

When playing radio or Spotify / Deezer / Tune-in radio stream the Albumart URL is not showed in the SOAP reply, there is no aac:... xxxx Uri where I can add / prefix http://IP:1400/ to get absolute URL. I think / assume i need to do a second (additional) http call and post a SOAP enveloppe with some previous recieved data. I'm kind of in the dank here.,

What service do you poll to get the get the CurrentMetadata Didl? It looks like it shows more data then the GetPositionInfo data

Do you poll the contentdir service for thIs? If yes how does your http post looks like?

I'm happy when I can get 150x150 image from player, higer RES is nice in a later stage..

Thanks again for your help!

BR,

Erwin van der Zwart

PS: If you need help on building automation topics feel free to ask, that's my origin specialty (: (KNX / BACnet / Modbus / Canbus / Enocean / Zigbee / BLE / Serial / TCP-IP / Web / Javascript / LUA)

jishi commented 8 years ago

Okay, did some investigation, and the albumart is indeed part of the event data, but not when you invoke the UPnP endpoints directly. Best way would be to act on the event data if possible, but that requires you to keep an HTTP endpoint available for Sonos to POST data into.

You can take a shortcut, bu instead composing the album art uri yourself. Invoke GetMediaInfo from the active player, then use the CurrentURI node from the response (this would look something like this: x-sonosapi-stream:s20308?sid=254&flags=32

Urlencode it (e.g x-sonosapi-stream%3as20308%3fsid%3d254%26flags%3d32), then use that value for the u option in the following url scheme: /getaa?s=1&u=x-sonosapi-stream%3as20308%3fsid%3d254%26flags%3d32, and send that to any player in your network on port 1400, like this: http://192.168.1.154:1400/getaa?s=1&u=x-sonosapi-stream%3as20308%3fsid%3d254%26flags%3d32

The album art scheme is fixed for these kind of images. Some services gives you high-res version here, other services requires direct invokation against the SMAPI to get a direct CDN url for the high res version.

ErwinvanderZwart commented 8 years ago

Hi Jimmy,

It's finally working (: Big thanks for your assist!

I was so close now that i see your solution.

I already tried this approach but I 'forgot' to do the urlencode on the CurrentURI value. Stupid (: I could know by the invalid chars..

Current resolution is good enough for now.

Again thanks!

BR,

Erwin van der Zwart

jishi commented 8 years ago

Glad I could help!

ErwinvanderZwart commented 8 years ago

Hi Jimmy,

I think you should know this in a second, but i browsed the complete UPNP and can't figure out where it is.

How can i get the next track uri that will be played next? Do i need to go through the queue and check what is playing now and grab the next track? In shuffle mode this won't work i guess. Where can i get this from the UPNP structure?

Thanks!

BR,

Erwin

jishi commented 8 years ago

Hi. When evented, you get both current and next track as part of the same message. I believe that there is a similar service method which would give you the same data (GetTransportInfo?) which I believe you already use? Can't verify now, but check if the response where you get currenturi doesn't contain info about next track as well.

ErwinvanderZwart commented 8 years ago

Hi Jimmy,

I think i know where the problem is comming from, i start a track with SetAVTransportURI

This causes the system to play the track as single track and not as track from the queue, and thats why it doesn't show the next track.

Do you know the correct command to start playing from queue when playing a favorite? When i already play from queue the seek function is working like expected.

I have tried x-rincon-queue:RINCON....#0 but this is not starting the queue when radio is playing.

I hope you an help me out with this last item i have.. (;

Thanks!

BR,

Erwin

ErwinvanderZwart commented 7 years ago

Hi Jimmy,

I want to create a SONOS doorbell function over UPNP, i can fetch the current queue, and i know the current track, to start playing again. after this i know how to seek correct position and start again.

But i don't know how to restore this fetched queue, so now my playlist is not continued ):

Do you have any tips how to do this? (not by node.js API, i use pure UPNP SOAP commands)

Do you know how to delete a created playlist with SOAP command?

Thanks!

BR,

Erwin

jishi commented 7 years ago

Hi.

I do this with the clip-command in the http-api. Basically what it does is:

Remember the state of the player (basically, the correct avtransport and metadata, and if queue playback, the track number and elapsed time).

Send a new SetAVTransportURI with just a url with the audio clip you want to play. It will take mp3, wav, flac or aac files, I think.

Wait for the clip to finish playing (easy if you have a fixed time clip, VERY hard if you need dynamic waiting, trust me on this one ;) )

Restore the previously stored avtransporturi and metadata (important if this was an online radio). If it was a queue (basically, if it started with x-rincon-queue), send the approriate calls for setting current track, and also elapsed time. Then call PLAY!

ErwinvanderZwart commented 7 years ago

Hi Jimmy,

Got it (; Tracknumber was the thing i was strugling with, it's now working, Thanks for pointing to this!

You might have a answer to another item i have, when grouping players i stop the coordinator, get volume, set this volume to the slave, add slave to coordinator and snapshot the group. After this is set uri and metadata and set mute to 0 and play group again.

All is working but the volume of the slave is not equal to coordinator but is reacting to group volume. Whatever i try i can't get the volumes in sync so they are all equal in the group.

Any idea what could cause that?

BR,

Erwin

jishi commented 7 years ago

My experience is that the volume stays put during grouping, so I find the behavior that you are describing to be odd.

How are you grouping them? what call do you make?

In my lib, I group them first and then set volume, but I don't think it should matter. And also, although Sonos has the notion of "group volume" calls, they aren't actually used by the official controller. The official controller sends individual volume calls, and so do I because the group volume is unpredictable.

ErwinvanderZwart commented 7 years ago

I set them like this (it's lua so you don't see complete function but gives you info you need)

function Add_Player_To_Group(ip, uuid) if ip and uuid then --ip = slave --uuid = master

    -- Get ip address of group master by uuid
    MasterData = Get_Current_Sonos_Player_UUID(uuid)
    MasterIP = MasterData.ip

    -- Stop playing on master
    --upnpavcmd(MasterIP, 1400, 'Stop')

    -- Get current master volume
    MasterVolume = upnpavcmd(MasterIP, 1400, 'GetVolume','<Channel>Master</Channel><DesiredVolume></DesiredVolume>')

    -- Wait 1/2 second to process changes
    os.sleep(0.5)

    -- Set volume on added player same as master volume
    upnpavcmd(ip, 1400, 'SetVolume', '<Channel>Master</Channel><DesiredVolume>' .. MasterVolume .. '</DesiredVolume>')

    -- Wait 1/2 second to process changes
    os.sleep(0.5)

    -- Get current uri from master
    position_info = upnpavcmd(MasterIP, 1400, 'GetPositionInfo')
    if position_info ~= nil then
        for i in string.gmatch(position_info, '<TrackURI.-</TrackURI>') do
            if i ~= nil then
                current_master_uri =  i:match([[<TrackURI>(.-)</TrackURI>]])
            else
                current_master_uri = 'UNKNOWN'
            end
        end
        for i in string.gmatch(position_info, '<TrackMetaData.-</TrackMetaData>') do
            if i ~= nil then
                current_master_uri_meta_data =  i:match([[<TrackMetaData>(.-)</TrackMetaData>]])
            else
                current_master_uri_meta_data = 'UNKNOWN'
            end
        end
    end

    -- Add new player to group
    uri = '<CurrentURI>x-rincon:' .. uuid .. '</CurrentURI><CurrentURIMetaData/>'
    upnpavcmd(ip, 1400, 'SetAVTransportURI', uri)

    -- Wait 1 second to process changes
    os.sleep(1)

    -- Make volume snapshot
    upnpavcmd(MasterIP, 1400, 'SnapshotGroupVolume')

    -- Set group volume
    upnpavcmd(MasterIP, 1400, 'SetGroupVolume', '<Channel>Master</Channel><DesiredVolume>' .. MasterVolume .. '</DesiredVolume>')

    -- Set mute on group as unmute to be sure all players are unmuted
    upnpavcmd(MasterIP, 1400, 'SetGroupMute', '<Channel>Master</Channel><DesiredMute>0</DesiredMute>')

    -- Start playing saved URI
    --MasterUri = '<CurrentURI>' .. current_master_uri .. '</CurrentURI>'
    --MasterMeta = MasterUri .. '<CurrentURIMetadata>' .. current_master_uri_meta_data .. '</CurrentURIMetaData>'
    --upnpavcmd(MasterIP, 1400, 'SetAVTransportURI', MasterMeta)

    -- Start playing again
    Play(MasterIP)

    return true
else
    return false
end

end

BR,

Erwin

jishi commented 7 years ago

It's hard to figure out from that part of the code. If you can make network captures somehow of what is actually sent, it would make more sense. But from the looks of it, I can't see anything concrete that differs from my process.