owntone / owntone-server

Linux/FreeBSD DAAP (iTunes) and MPD audio server with support for AirPlay 1 and 2 speakers (multiroom), Apple Remote (and compatibles), Chromecast, Spotify and internet radio.
https://owntone.github.io/owntone-server
GNU General Public License v2.0
2.09k stars 237 forks source link

controlling daapd from shell #9

Closed snizzleorg closed 10 years ago

snizzleorg commented 10 years ago

this not really an issue...but a small request to point me where I can find infos on how to control daapd from the command line. I found this here:

http://www.raspberrypi.org/phpBB3/viewtopic.php?f=66&t=49928 curl "http://localhost:3689/login?pairing-guid=0x1" curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x7'&container-item-spec='dmap.containeritemid:0x10402'&session-id=100"

which is nice to start a stream. But ideally I would like to do a little more like stop it again and select speakers.

Maybe there is even some python or ruby or whatnot library that can do this but I haven't found anything. Any ideas where to look for this?

Thanks

jkiddo commented 10 years ago

If you don't mind doing a little coding I have a Java API that you might find usefull

snizzleorg commented 10 years ago

well it might be worth a try. I never wrote anything in Java though.

btw. I managed to stop the playing by issuing

http://localhost:3689/ctrl-int/1/pause?session-id=100

i still think there should be the some syntax to do other things (like select speakers but i can't find it anywhere...

ejurgensen commented 10 years ago

If Java is an option I think jkiddo's client is the best bet, but if you're after a Ruby solution I once came across this: https://github.com/jurriaan/ruby-dacpclient

The command for selecting speakers is called setspeakers. A good way to learn the syntax is to set log level to debug, do the stuff you want to implement with Remote, and then grabbing the commands from the log (grep DAAP and grep DACP).

jkiddo commented 10 years ago

I just read through the Ruby solution at it seems to be a pretty valid solution - and it supports more features than the Java one (guess I'll have to start doing some Ruby to Java porting ... ;) )

snizzleorg commented 10 years ago

I tried the ruby-dacpclient but with no success something seams to wrong with my ruby install or some dependencies...

thats what I get when I run the example /usr/local/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require': cannot load such file -- dbm (LoadError)

I'll ask juriaan about it. thanks anyway for pointing me to dacpclient.

snizzleorg commented 10 years ago

small update:

sessionid=$(curl -s "http://localhost:3689/login?pairing-guid=0x1" | /home/steffen/src/dacp/dacp_sessionid_decode.py)
curl "http://localhost:3689/ctrl-int/1/setspeakers?speaker-id=0xC467B5080CAA,0x1b63235a8b&session-id=$sessionid"
curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x7'&container-item-spec='dmap.containeritemid:0x28A2'&session-id=$sessionid"

this is working so far. the dacp_sessionid_decode.py is a script I adapted from:

#!/usr/bin/python

# simple message decoder for dacp
# released gplv3 by jeffrey sharkey

import sys, struct, re

raw = []

#for c in raw_input(): raw.append(c)
#raw.append('\x0a')

for c in sys.stdin.read(): raw.append(c)

def format(c):
        if ord(c) >= 128: return "(byte)0x%02x"%ord(c)
        else: return "0x%02x"%ord(c)

print ','.join([ format(c) for c in raw ])

def read(queue, size):
        pull = ''.join(queue[0:size])
        del queue[0:size]
        return pull

group = ['casp','cmst','mlog','agal','mlcl','mshl','mlit','abro','abar','apso','caci','avdb','cmgt','aply','adbs','cmpa']

rebinary = re.compile('[^\x20-\x7e]')

def ashex(s): return ''.join([ "%02x" % ord(c) for c in s ])

def asbyte(s): return struct.unpack('>B', s)[0]
def asint(s): return struct.unpack('>I', s)[0]
def aslong(s): return struct.unpack('>Q', s)[0]

def decode(raw, handle, indent):
        while handle >= 8:

                # read word data type and length
                ptype = read(raw, 4)
                plen = asint(read(raw, 4))
                handle -= 8 + plen

                # recurse into groups
                if ptype in group:
                        print '\t' * indent, ptype, " --+"
                        decode(raw, plen, indent + 1)
                        continue

                # read and parse data
                pdata = read(raw, plen)

                nice = '%s' % ashex(pdata)
                if plen == 1: nice = '%s == %s' % (ashex(pdata), asbyte(pdata))
                if plen == 4: nice = '%s == %s' % (ashex(pdata), asint(pdata))
                if plen == 8: nice = '%s == %s' % (ashex(pdata), aslong(pdata))

                if rebinary.search(pdata) is None:
                        nice = pdata

                print '\t' * indent, ptype.ljust(6), str(plen).ljust(6), nice

decode(raw, len(raw), 0)

my crude adaptation just prints out the session-id:

#!/usr/bin/python

# simple message decoder for dacp
# released gplv3 by jeffrey sharked
# adapted by universaldilettant to only print the session-id

import sys, struct, re

raw = []

#for c in raw_input(): raw.append(c)
#raw.append('\x0a')

for c in sys.stdin.read(): raw.append(c)

def format(c):
        if ord(c) >= 128: return "(byte)0x%02x"%ord(c)
        else: return "0x%02x"%ord(c)

#print ','.join([ format(c) for c in raw ])

def read(queue, size):
        pull = ''.join(queue[0:size])
        del queue[0:size]
        return pull

group = ['casp','cmst','mlog','agal','mlcl','mshl','mlit','abro','abar','apso','caci','avdb','cmgt','aply','adbs','cmpa']

rebinary = re.compile('[^\x20-\x7e]')

def ashex(s): return ''.join([ "%02x" % ord(c) for c in s ])

def asbyte(s): return struct.unpack('>B', s)[0]
def asint(s): return struct.unpack('>I', s)[0]
def aslong(s): return struct.unpack('>Q', s)[0]

def decode(raw, handle, indent):
        while handle >= 8:

                # read word data type and length
                ptype = read(raw, 4)
                plen = asint(read(raw, 4))
                handle -= 8 + plen

                # recurse into groups
                if ptype in group:
                        #print '\t' * indent, ptype, " --+"
                        decode(raw, plen, indent + 1)
                        continue

                # read and parse data
                pdata = read(raw, plen)

                nice = '%s' % ashex(pdata)
                if plen == 1: nice = '%s == %s' % (ashex(pdata), asbyte(pdata))
                if plen == 4: nice = '%s == %s' % (ashex(pdata), asint(pdata))
                if plen == 8: nice = '%s == %s' % (ashex(pdata), aslong(pdata))

                if rebinary.search(pdata) is None:
                        nice = pdata
                if str(ptype.ljust(6)) == "mlid  ":
                        #print '\t' * indent, ptype.ljust(6), str(plen).ljust(6), nice
                        print asint(pdata)

decode(raw, len(raw), 0)
snizzleorg commented 10 years ago

btw. I found the easiest to get the speaker id's is to call avahi-browse

avahi-browse _raop._tcp.

which puts out a list of airplay devices like this:

+   eth0 IPv4 28E7CFEF7820@beamer (7)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (5)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (3)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (2)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (6)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (4)                       AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer                           AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (428)                     AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (427)                     AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (397)                     AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (391)                     AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (357)                     AirTunes Remote Audio local
+   eth0 IPv4 28E7CFEF7820@beamer (331)                     AirTunes Remote Audio local
+   eth0 IPv4 0011245F6041@gabstar                          AirTunes Remote Audio local
+   eth0 IPv4 F0D1A90A45C1@living                           AirTunes Remote Audio local
+   eth0 IPv4 001B63235A8B@kitchentunes                     AirTunes Remote Audio local

no idea why the beamer (an AppleTV) is showing up so often

ejurgensen commented 10 years ago

Cool! Actually I made a modification a few days ago, so that you in the login can request a session_id. This change is in the pipe branch, which will soon be merged with master. The requested id has to be < 100. The idea is to avoid the need to parse the result from the login, so you can do like this:

curl "http://localhost:3689/login?pairing-guid=0x1&request-session-id=50"
curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x[PLAYLIST-ID]'&container-item-spec='dmap.containeritemid:0x[FILE ID]'&session-id=50"
curl "http://localhost:3689/logout?session-id=50"

Of course, your approach is the proper way to do it. Using the daap parser is probably also going to be useful if you want to do some more sophisticated command line tasks, like for instance getting a list of songs and then selecting one of them.

snizzleorg commented 10 years ago

nice!

only my approach is a dirty hack somehow. But I guess the logout is probably something I'll add anyway. How long is a session valid normally?

right now every command I issue creates a new session - that works but is probably not the best way to do it.

I'll try to see if I can get the decoding and curl(ing) done properly in a python script... That actually would be nice. But now all I wanted was to start the Radio on two speakers in the house in the morning. And that is working perfectly now.

snizzleorg commented 10 years ago

do you have a list of available functions somewhere? can I get this form your source code? I'm basically looking for some function to determine whether forked-daapd is playing. I want to switch on my stereo based on whether forked-daapd is playing and maybe switch it off when the music stops...

ejurgensen commented 10 years ago

You will need to look in httpd_dacp.c (line 1941) and httpd_daap.c (line 2607). I think the call you are looking for is the one in dacp.c called playstatusupdate. Use revision number 1 to get an immediate reply. The reply will be daap encoded, so it has to be parsed (look at line 223 in dacp.c for the tag you are interested in).

jkiddo commented 10 years ago

If you are more comfortable with looking at Java, have a look at the project called Jolivia (authored by me) - that ought to point out the url's that iRemote calls/uses

snizzleorg commented 10 years ago

hm. I'm not really a programmer. I find my way around more or less with python and a little ansi C but java I never looked into... But thanks anyways.