<?php /** * PHP class to control Sonos * Forked from http://www.github.com/DjMomo/sonos * Forked repo / updates from https://github.com/phil-lavin/sonos * * Available functions : * - Play() : play / lecture * - Pause() : pause * - Stop() : stop * - Next() : next track / titre suivant * - Previous() : previous track / titre précédent * - SeekTime(string) : seek to time xx:xx:xx / avancer-reculer à la position xx:xx:xx * - ChangeTrack(int) : change to track xx / aller au titre xx * - RestartTrack() : restart actual track / revenir au début du titre actuel * - RestartQueue() : restart queue / revenir au début de la liste actuelle * - GetVolume() : get volume level / récupérer le niveau sonore actuel * - SetVolume(int) : set volume level / régler le niveau sonore * - GetMute() : get mute status / connaitre l'état de la sourdine * - SetMute(bool) : active-disable mute / activer-désactiver la sourdine * - GetTransportInfo() : get status about player / connaitre l'état de la lecture * - GetMediaInfo() : get informations about media / connaitre des informations sur le média * - GetPositionInfo() : get some informations about track / connaitre des informations sur le titre * - AddURIToQueue(string,bool) : add a track to queue / ajouter un titre à la liste de lecture * - RemoveTrackFromQueue(int) : remove a track from Queue / supprimer un tritre de la liste de lecture * - RemoveAllTracksFromQueue() : remove all tracks from queue / vider la liste de lecture * - RefreshShareIndex() : refresh music library / rafraichit la bibliothèque musicale * - SetQueue(string) : load a track or radio in player / charge un titre ou une radio dans le lecteur * - PlayTTS(string message,string station,int volume,string lang) : play a text-to-speech message / lit un message texte * * Functions only available in the fork: * - static get_room_coordinator(string room_name) : Returns an instance of SonosPHPController representing the 'coordinator' of the specified room * - static detect(string ip,string port) : IP and port are optional. Returns an array of instances of SonosPHPController, one for each Sonos device found on the network * - get_coordinator() : Returns an instance of SonosPHPController representing the 'coordinator' of the room this device is in * - device_info() : Gets some info about this device as an array * - AddSpotifyToQueue(string spotify_id,bool next) : Adds the provided spotify ID to the queue either next or at the end */ class SonosPHPController { protected $Sonos_IP; protected $_raw = array(); /** * Constructeur * @param string Sonos IP adress * @param string Sonos port (optional) */ public function __construct($Sonos_IP,$Sonos_Port = '1400') { // On assigne les paramètres aux variables d'instance. $this->IP = $Sonos_IP; $this->PORT = $Sonos_Port; } protected function Upnp($url,$SOAP_service,$SOAP_action,$SOAP_arguments = '',$XML_filter = '') { $POST_xml = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'; $POST_xml .= '<s:Body>'; $POST_xml .= '<u:'.$SOAP_action.' xmlns:u="'.$SOAP_service.'">'; $POST_xml .= $SOAP_arguments; $POST_xml .= '</u:'.$SOAP_action.'>'; $POST_xml .= '</s:Body>'; $POST_xml .= '</s:Envelope>'; $POST_url = $this->IP.":".$this->PORT.$url; $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_URL, $POST_url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml", "SOAPAction: ".$SOAP_service."#".$SOAP_action)); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $POST_xml); $r = curl_exec($ch); curl_close($ch); if ($XML_filter != '') return $this->Filter($r,$XML_filter); else return $r; } protected function Filter($subject,$pattern) { preg_match('/\<'.$pattern.'\>(.+)\<\/'.$pattern.'\>/',$subject,$matches); ///'/\<'.$pattern.'\>(.+)\<\/'.$pattern.'\>/' return $matches[1]; } /** * Play */ public function Play() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Play'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID><Speed>1</Speed>'; return $this->Upnp($url,$service,$action,$args); } /** * Pause */ public function Pause() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Pause'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; return $this->Upnp($url,$service,$action,$args); } /** * Stop */ public function Stop() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Stop'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; return $this->Upnp($url,$service,$action,$args); } /** * Next */ public function Next() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Next'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; return $this->Upnp($url,$service,$action,$args); } /** * Previous */ public function Previous() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Previous'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; return $this->Upnp($url,$service,$action,$args); } /** * Seek to position xx:xx:xx or track number x * @param string 'REL_TIME' for time position (xx:xx:xx) or 'TRACK_NR' for track in actual queue * @param string */ public function Seek($type,$position) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Seek'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID><Unit>'.$type.'</Unit><Target>'.$position.'</Target>'; return $this->Upnp($url,$service,$action,$args); } /** * Seek to time xx:xx:xx */ public function SeekTime($time) { return $this->Seek("REL_TIME",$time); } /** * Change to track number */ public function ChangeTrack($number) { return $this->Seek("TRACK_NR",$number); } /** * Restart actual track */ public function RestartTrack() { return $this->Seek("REL_TIME","00:00:00"); } /** * Restart actual queue */ public function RestartQueue() { return $this->Seek("TRACK_NR","1"); } /** * Get volume value (0-100) */ public function GetVolume() { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'GetVolume'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '<InstanceID>0</InstanceID><Channel>Master</Channel>'; $filter = 'CurrentVolume'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Set volume value (0-100) */ public function SetVolume($volume) { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'SetVolume'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>'.$volume.'</DesiredVolume>'; return $this->Upnp($url,$service,$action,$args); } /** * Get mute status */ public function GetMute() { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'GetMute'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '<InstanceID>0</InstanceID><Channel>Master</Channel>'; $filter = 'CurrentMute'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Set mute * @param integer mute active=1 */ public function SetMute($mute = 0) { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'SetMute'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>'.$mute.'</DesiredMute>'; return $this->Upnp($url,$service,$action,$args); } /** * Get Transport Info : get status about player */ public function GetTransportInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetTransportInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; $filter = 'CurrentTransportState'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Get Media Info : get informations about media */ public function GetMediaInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetMediaInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; $filter = 'CurrentURI'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Get Position Info : get some informations about track */ public function GetPositionInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetPositionInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; $xml = $this->Upnp($url,$service,$action,$args); $data["TrackNumberInQueue"] = $this->Filter($xml,"Track"); $data["TrackURI"] = $this->Filter($xml,"TrackURI"); $data["TrackDuration"] = $this->Filter($xml,"TrackDuration"); $data["RelTime"] = $this->Filter($xml,"RelTime"); $TrackMetaData = $this->Filter($xml,"TrackMetaData"); $xml = substr($xml, stripos($TrackMetaData, '<')); $xml = substr($xml, 0, strrpos($xml, '>') + 4); $xml = str_replace(array("<", ">", """, "&", "%3a", "%2f", "%25"), array("<", ">", "\"", "&", ":", "/", "%"), $xml); $data["Title"] = $this->Filter($xml,"dc:title"); // Track Title $data["AlbumArtist"] = $this->Filter($xml,"r:albumArtist"); // Album Artist $data["Album"] = $this->Filter($xml,"upnp:album"); // Album Title $data["TitleArtist"] = $this->Filter($xml,"dc:creator"); // Track Artist return $data; } /** * Add URI to Queue * @param string track/radio URI * @param bool added next (=1) or end queue (=0) */ public function AddURIToQueue($URI,$next=0) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'AddURIToQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $next = (int)$next; $args = '<InstanceID>0</InstanceID><EnqueuedURI>'.$URI.'</EnqueuedURI><EnqueuedURIMetaData></EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>'.$next.'</EnqueueAsNext>'; $filter = 'FirstTrackNumberEnqueued'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Remove a track from Queue * */ public function RemoveTrackFromQueue($tracknumber) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'RemoveTrackFromQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID><ObjectID>Q:0/'.$tracknumber.'</ObjectID>'; return $this->Upnp($url,$service,$action,$args); } /** * Clear Queue * */ public function RemoveAllTracksFromQueue() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'RemoveAllTracksFromQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID>'; return $this->Upnp($url,$service,$action,$args); } /** * Set Queue * @param string URI of new track */ public function SetQueue($URI) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'SetAVTransportURI'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '<InstanceID>0</InstanceID><CurrentURI>'.$URI.'</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>'; return $this->Upnp($url,$service,$action,$args); } /** * Refresh music library * */ public function RefreshShareIndex() { $url = '/MediaServer/ContentDirectory/Control'; $action = 'RefreshShareIndex'; $service = 'urn:schemas-upnp-org:service:ContentDirectory:1'; return $this->Upnp($url,$service,$action,$args); } /** * Split string in several strings * */ protected function CutString($string,$intmax) { $i = 0; while (strlen($string) > $intmax) { $string_cut = substr($string, 0, $intmax); $last_space = strrpos($string_cut, "+"); $strings[$i] = substr($string, 0, $last_space); $string = substr($string, $last_space, strlen($string)); $i++; } $strings[$i] = $string; return $strings; } /** * Convert Words (text) to Speech (MP3) * */ protected function TTSToMp3($words,$lang) { // Directory $folder = "audio/".$lang; // Replace the non-alphanumeric characters // The spaces in the sentence are replaced with the Plus symbol $words = urlencode($words); // Name of the MP3 file generated using the MD5 hash $file = md5($words); // If folder doesn't exists, create it if (!file_exists($folder)) mkdir($folder, 0755, true); // Save the MP3 file in this folder with the .mp3 extension $file = $folder."/TTS-".$file.".mp3"; // If the MP3 file exists, do not create a new request if (!file_exists($file)) { // Google Translate API cannot handle strings > 100 characters $words = $this->CutString($words,100); ini_set('user_agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0'); $mp3 = ""; for ($i = 0; $i < count($words); $i++) $mp3[$i] = file_get_contents('http://translate.google.com/translate_tts?q='.$words[$i].'&tl='.$lang); file_put_contents($file, $mp3); } return $file; } /** * Say song name via TTS message * @param string message * @param string radio name display on sonos controller * @param int volume * @param string language */ public function SongNameTTS($directory,$volume=0,$unmute=0,$lang='fr') { $ThisSong = "Cette chanson s'appelle "; $By = " de "; $actual['track'] = $this->GetPositionInfo(); $SongName = $actual['track']['Title']; $Artist = $actual['track']['TitleArtist']; $message = $ThisSong . $SongName . $By . $Artist ; $this->PlayTTS($message,$directory,$volume,$unmute,$lang); return true; } /** * Play a TTS message * @param string message * @param string radio name display on sonos controller * @param int volume * @param string language */ public function PlayTTS($message,$directory,$volume=0,$unmute=0,$lang='fr') { $actual['track'] = $this->GetPositionInfo(); $actual['volume'] = $this->GetVolume(); $actual['mute'] = $this->GetMute(); $actual['status'] = $this->GetTransportInfo(); $this->Pause(); if ($unmute == 1) $this->SetMute(0); if ($volume != 0) $this->SetVolume($volume); $file = 'x-file-cifs://'.$directory.'/'.$this->TTSToMp3($message,$lang); if (((stripos($actual['track']["TrackURI"],"x-file-cifs://")) != false) or ((stripos($actual['track']["TrackURI"],".mp3")) != false)) { // It's a MP3 file $TrackNumber = $this->AddURIToQueue($file); $this->ChangeTrack($TrackNumber); $this->Play(); sleep(2); while ($this->GetTransportInfo() == "PLAYING") {} $this->Pause(); $this->SetVolume($actual['volume']); $this->SetMute($actual['mute']); $this->ChangeTrack($actual['track']["TrackNumberInQueue"]); $this->SeekTime($actual['track']["RelTime"]); $this->RemoveTrackFromQueue($TrackNumber); } else { //It's a radio / or TV (playbar) / or nothing $this->SetQueue($file); $this->Play(); sleep(2); while ($this->GetTransportInfo() == "PLAYING") {} $this->Pause(); $this->SetVolume($actual['volume']); $this->SetMute($actual['mute']); $this->SetQueue($actual['track']["TrackURI"]); } if (strcmp($actual['status'],"PLAYING") == 0) $this->Play(); return true; } public function AddSpotifyToQueue($spotify_id, $next = false) { $rand = mt_rand(10000000, 99999999); $meta = '<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="'.$rand.'spotify%3atrack%3a'.$spotify_id.'" restricted="true"> <dc:title></dc:title> <upnp:class>object.item.audioItem.musicTrack</upnp:class> <desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON2311_X_#Svc2311-0-Token</desc> </item> </DIDL-Lite>'; $meta = htmlentities($meta); $url = '/MediaRenderer/AVTransport/Control'; $action = 'AddURIToQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $next = (int)$next; $args = " <InstanceID>0</InstanceID> <EnqueuedURI>x-sonos-spotify:spotify%3atrack%3a{$spotify_id}</EnqueuedURI> <EnqueuedURIMetaData>{$meta}</EnqueuedURIMetaData> <DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued> <EnqueueAsNext>{$next}</EnqueueAsNext> "; $filter = 'FirstTrackNumberEnqueued'; return $this->Upnp($url, $service, $action, $args, $filter); } public function device_info() { $xml = $this->_device_info_raw('/xml/device_description.xml'); $out = array( 'friendlyName' => (string)$xml->device->friendlyName, 'modelNumber' => (string)$xml->device->modelNumber, 'modelName' => (string)$xml->device->modelName, 'softwareVersion' => (string)$xml->device->softwareVersion, 'hardwareVersion' => (string)$xml->device->hardwareVersion, 'roomName' => (string)$xml->device->roomName, ); return $out; } public function get_coordinator() { $topology = $this->_device_info_raw('/status/topology'); $myself = null; $coordinators = array(); // Loop players, build map of coordinators and find myself foreach ($topology->ZonePlayers->ZonePlayer as $player) { $player_data = $player->attributes(); $url = parse_url((string)$player_data->location); $ip = $url['host']; if ($ip == $this->IP) { $myself = $player_data; } if ((string)$player_data->coordinator == 'true') { $coordinators[(string)$player_data->group] = $ip; } } $coordinator = $coordinators[(string)$myself->group]; return new static($coordinator); } protected function _device_info_raw($url) { $url = "http://{$this->IP}:{$this->PORT}{$url}"; if (!isset($this->_raw[$url])) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch); curl_close($ch); $this->_raw[$url] = simplexml_load_string($data); } return $this->_raw[$url]; } public static function detect($ip = '239.255.255.250', $port = 1900) { $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); socket_set_option($sock, getprotobyname('ip'), IP_MULTICAST_TTL, 2); $data = <<<DATA M-SEARCH * HTTP/1.1 HOST: {$ip}:reservedSSDPport MAN: ssdp:discover MX: 1 ST: urn:schemas-upnp-org:device:ZonePlayer:1 DATA; socket_sendto($sock, $data, strlen($data), null, $ip, $port); // All passed by ref $read = array($sock); $write = $except = array(); $name = $port = null; $tmp = ''; // Read buffer $buff = ''; // Loop until there's nothing more to read while (socket_select($read, $write, $except, 1) && $read) { socket_recvfrom($sock, $tmp, 2048, null, $name, $port); $buff .= $tmp; } // Parse buffer into devices $data = static::_parse_detection_replies($buff); // Make an array of myselfs $devices = array(); foreach ($data as $datum) { $url = parse_url($datum['location']); $devices[] = new static($url['host'], $url['port']); } return $devices; } protected static function _parse_detection_replies($replies) { $out = array(); // Loop each reply foreach (explode("\r\n\r\n", $replies) as $reply) { if ( ! $reply) { continue; } // New array entry $arr =& $out[]; // Loop each line foreach (explode("\r\n", $reply) as $line) { // End of header name if (($colon = strpos($line, ':')) !== false) { $name = strtolower(substr($line, 0, $colon)); $val = trim(substr($line, $colon + 1)); $arr[$name] = $val; } } } return $out; } public static function get_room_coordinator($room_name) { // Detect devices. Sometimes takes a few goes. do { $devices = static::detect(); if (!$devices) { sleep(1); } } while (!$devices); foreach ($devices as $device) { $dev = $device->device_info(); if ($dev['roomName'] == $room_name) { return $device->get_coordinator(); } } return false; } }