Synology Audio Station Web API

Synology(Edit)

If you are looking for APIs, probably you know what Synology is about. The NAS software offers a comfortable UI and functionalities.

Audio Station(Edit)

Audio Station handles heavy music libraries quite well, also provides a DLNA/UPNP client, which allows to control a Raspberry Pi, at best with a DAC, running gmrender-resurrect as a headless music server!

Scripting(Edit)

So, all results combined, what can we hope for? The following script presents a few interactions from the ones discovered. This allows for remote and fully automated interaction with a Smart Home deployment. More work has to be done, searches to be conducted, but already what has been done is presented under this small doc/disclaimer:

Usage: synoApi.py json:arguments
    JSON Object formated:
        action: <play|stop|pause|push|get_title>
        playlist: playlist_name (only for push, optional)

    Example of use in terminal:
        python synoApi.py \{\"action\":\"push\"\,\"playlist\":\"Ambiance\"\}
        python synoApi.py \{\"action\":\"play\"\}

NB: The thought, behind using JSON input, was to be able to take raw inputs
and interact with Node Red, the rule engine for home automation.
No security has been put into it, credentials are in cleartext,
supposed to be a proof-of-concept, adapt to your needs...

Python (plain)
  1. #!/bin/python2.7
  2.  
  3. import os
  4. import urllib2, cookielib
  5. import urllib
  6. import json
  7. import sys
  8.  
  9. cj = cookielib.CookieJar()
  10. opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
  11.  
  12. token = False
  13. player = False
  14. playlist = False
  15.  
  16. # CONFIG
  17.  
  18. username = "&lt;username>"
  19. password = "&lt;password>"
  20.  
  21. host = "&lt;example.com>"
  22. port = "5001"
  23. prot = "https"
  24.  
  25. playerName = "&lt;player_name>"
  26. playlistName = "&lt;default_playlist_name>"
  27.  
  28. # ENDCONFIG
  29.  
  30. def baseUrl():
  31. return "{0}://{1}:{2}/webapi/".format(prot, host, port)
  32.  
  33. def paramsToUrl(params):
  34. params = urllib.urlencode(params)
  35. return params
  36.  
  37. def login():
  38. payload = { 'api':'SYNO.API.Auth', 'method':'login', 'version':2, 'session':'AudioStation', 'format':'sid', 'account':username, 'passwd':password }
  39. r = opener.open("{0}auth.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  40. j = json.loads(r.read())
  41.  
  42. if 'error' in j.keys():
  43. print(str(j))
  44.  
  45. if 'data' in j.keys() and 'sid' in j['data'].keys():
  46. return j['data']['sid']
  47.  
  48. return False
  49.  
  50. def getPlayerFromName(token, needle):
  51. payload = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'list', 'version':2, 'sid':token }
  52. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  53. j = json.loads(r.read())
  54.  
  55. if 'error' in j.keys():
  56. print(str(j))
  57.  
  58. if 'data' in j.keys() and 'players' in j['data'].keys():
  59. for player in j['data']['players']:
  60. if player['name'] == needle:
  61. return player['id']
  62.  
  63. return False
  64.  
  65. def getPlaylistFromName(token, needle):
  66. payload = { 'api':'SYNO.AudioStation.Playlist', 'method':'list', 'version':2, 'sid':token }
  67. r = opener.open("{0}AudioStation/playlist.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  68. j = json.loads(r.read())
  69.  
  70. if 'error' in j.keys():
  71. print(str(j))
  72.  
  73. if 'data' in j.keys() and 'playlists' in j['data'].keys():
  74. for playlist in j['data']['playlists']:
  75. if playlist['name'] == needle:
  76. return { 'id':playlist['id'], 'library':playlist['library'], 'offset':j['data']['offset'] }
  77.  
  78. return False
  79.  
  80. def play(token, player):
  81. payload = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'control', 'id':player, 'action':'play', 'version':2, 'sid':token }
  82.  
  83. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  84. j = json.loads(r.read())
  85.  
  86. if 'error' in j.keys():
  87. print(str(j))
  88.  
  89. def pause(token, player):
  90. payload = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'control', 'id':player, 'action':'pause', 'version':2, 'sid':token }
  91.  
  92. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  93. j = json.loads(r.read())
  94.  
  95. if 'error' in j.keys():
  96. print(str(j))
  97.  
  98. def stop(token, player):
  99. payload = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'control', 'id':player, 'action':'stop', 'version':2, 'sid':token }
  100.  
  101. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  102. j = json.loads(r.read())
  103.  
  104. if 'error' in j.keys():
  105. print(str(j))
  106.  
  107. def pull(token, player, target):
  108. needles = target.split('.') if '.' in target else [ target ]
  109. payload = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'getstatus', 'id':player, 'version':2, 'sid':token }
  110.  
  111. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  112. j = json.loads(r.read())
  113.  
  114. if 'data' in j.keys() and j['data'] is not None:
  115. if len(needles) == 1 and needles[0] in j['data'].keys():
  116. return j['data'][needles[0]]
  117. elif len(needles) == 2 and needles[0] in j['data'].keys():
  118. if j['data'][needles[0]] is not None and needles[1] in j['data'][needles[0]].keys():
  119. return j['data'][needles[0]][needles[1]]
  120. else:
  121. return j['data'][needles[0]]
  122. else:
  123. return j['data']
  124. else:
  125. print(str(j))
  126.  
  127. def push(token, player, playlist):
  128. tmp = { 'api':'SYNO.AudioStation.RemotePlayer', 'method':'updateplaylist', 'id':player, 'limit':0, 'play':'true', 'version':2, 'sid':token, 'containers_json':'[{{"type":"playlist","id":"{0}"}}]'.format(str(playlist['id'])) }
  129.  
  130. payload = playlist.copy()
  131. payload.update(tmp)
  132.  
  133. r = opener.open("{0}AudioStation/remote_player.cgi?{1}".format(baseUrl(), paramsToUrl(payload)))
  134. j = json.loads(r.read())
  135.  
  136. if 'error' in j.keys():
  137. print(str(j))
  138.  
  139. def getPlayingTitle(token, player):
  140. return pull(token, player, 'song.title')
  141.  
  142. def actionSelector():
  143. if len(sys.argv) &lt; 2:
  144. val = 0
  145. else:
  146. val = json.loads(sys.argv[1])['action']
  147.  
  148. actions = {
  149. 'play': 1,
  150. 'stop': 2,
  151. 'push': 3,
  152. 'pause': 4,
  153. 'get_title': 5
  154. }
  155.  
  156. return actions.get(val, 0)
  157.  
  158. if actionSelector() > 0:
  159. action = actionSelector()
  160. else:
  161. exit("No action selected")
  162.  
  163.  
  164. # authentication token
  165. token = login()
  166.  
  167. if token is not False:
  168. player = getPlayerFromName(token, playerName)
  169. else:
  170. exit('Login failed')
  171.  
  172. if player is not False:
  173. playlist = getPlaylistFromName(token, json.loads(sys.argv[1])['playlist'] if 'playlist' in json.loads(sys.argv[1]).keys() else playlistName)
  174. else:
  175. exit("Player not found")
  176.  
  177. if playlist is False:
  178. exit("Playlist not found")
  179.  
  180.  
  181. if action == 1:
  182. play(token, player)
  183. elif action == 2:
  184. stop(token, player)
  185. elif action == 3:
  186. push(token, player, playlist)
  187. elif action == 4:
  188. pause(token, player)
  189. elif action == 5:
  190. print('{0}'.format(getPlayingTitle(token, player).encode('utf-8)')))
  191.  
  192.  
  193. exit(0)

Thanks to all the people taking time to share their work. Hope this helps a few adventurers! To be completed, ratings, volume, searches are to be discovered!