1
0
Fork 0
mirror of https://github.com/Yetangitu/ampache synced 2025-10-03 17:59:21 +02:00

Add XBMC Localplay

This commit is contained in:
Afterster 2013-11-02 15:17:34 +01:00 committed by Paul Arthur
parent 872012a24f
commit 539ca08efc
18 changed files with 1703 additions and 0 deletions

View file

@ -72,6 +72,7 @@ Ampache includes some external modules that carry their own licensing.
* [Prototype](http://www.prototypejs.org/): MIT
* [Snoopy](http://snoopy.sourceforge.net/): LGPL v2.1
* [Whatever:hover](http://www.xs4all.nl/~peterned): LGPL v2.1
* [xbmc-php-rpc](https://github.com/karlrixon/xbmc-php-rpc): GPL v3
Translations
------------

View file

@ -0,0 +1,633 @@
<?php
/* vim:set softtabstop=4 shiftwidth=4 expandtab: */
/**
*
* LICENSE: GNU General Public License, version 2 (GPLv2)
* Copyright 2001 - 2013 Ampache.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License v2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
/**
* AmpacheXbmc Class
*
* This is the class for the xbmc localplay method to remote control
* a XBMC Instance
*
*/
class AmpacheXbmc extends localplay_controller {
/* Variables */
private $version = '000001';
private $description = 'Controls a XBMC instance';
/* Constructed variables */
private $_xbmc;
// Always use player 0 for now
private $_playerId = 0;
// Always use playlist 0 for now
private $_playlistId = 0;
/**
* Constructor
* This returns the array map for the localplay object
* REQUIRED for Localplay
*/
public function __construct() {
/* Do a Require Once On the needed Libraries */
require_once Config::get('prefix') . '/modules/xbmc-php-rpc/TCPClient.php';
} // Constructor
/**
* get_description
* This returns the description of this localplay method
*/
public function get_description() {
return $this->description;
} // get_description
/**
* get_version
* This returns the current version
*/
public function get_version() {
return $this->version;
} // get_version
/**
* is_installed
* This returns true or false if xbmc controller is installed
*/
public function is_installed() {
$sql = "DESCRIBE `localplay_xbmc`";
$db_results = Dba::query($sql);
return Dba::num_rows($db_results);
} // is_installed
/**
* install
* This function installs the xbmc localplay controller
*/
public function install() {
$sql = "CREATE TABLE `localplay_xbmc` (`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , ".
"`name` VARCHAR( 128 ) COLLATE utf8_unicode_ci NOT NULL , " .
"`owner` INT( 11 ) NOT NULL, " .
"`host` VARCHAR( 255 ) COLLATE utf8_unicode_ci NOT NULL , " .
"`port` INT( 11 ) UNSIGNED NOT NULL , " .
"`user` VARCHAR( 255 ) COLLATE utf8_unicode_ci NOT NULL , " .
"`pass` VARCHAR( 255 ) COLLATE utf8_unicode_ci NOT NULL" .
") ENGINE = MYISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
$db_results = Dba::query($sql);
// Add an internal preference for the users current active instance
Preference::insert('xbmc_active','XBMC Active Instance','0','25','integer','internal');
User::rebuild_all_preferences();
return true;
} // install
/**
* uninstall
* This removes the localplay controller
*/
public function uninstall() {
$sql = "DROP TABLE `localplay_xbmc`";
$db_results = Dba::query($sql);
// Remove the pref we added for this
Preference::delete('xbmc_active');
return true;
} // uninstall
/**
* add_instance
* This takes key'd data and inserts a new xbmc instance
*/
public function add_instance($data) {
$sql = "INSERT INTO `localplay_xbmc` (`name`,`host`,`port`, `user`, `pass`,`owner`) " .
"VALUES (?, ?, ?, ?, ?, ?)";
$db_results = Dba::query($sql, array($data['name'], $data['host'], $data['port'], $data['user'], $data['pass'], $GLOBALS['user']->id));
return $db_results;
} // add_instance
/**
* delete_instance
* This takes a UID and deletes the instance in question
*/
public function delete_instance($uid) {
$sql = "DELETE FROM `localplay_xbmc` WHERE `id` = ?";
$db_results = Dba::query($sql, array($uid));
return true;
} // delete_instance
/**
* get_instances
* This returns a key'd array of the instance information with
* [UID]=>[NAME]
*/
public function get_instances() {
$sql = "SELECT * FROM `localplay_xbmc` ORDER BY `name`";
$db_results = Dba::query($sql);
$results = array();
while ($row = Dba::fetch_assoc($db_results)) {
$results[$row['id']] = $row['name'];
}
return $results;
} // get_instances
/**
* update_instance
* This takes an ID and an array of data and updates the instance specified
*/
public function update_instance($uid,$data) {
$sql = "UPDATE `localplay_xbmc` SET `host` = ?, `port` = ?, `name` = ?, `user` = ?, `pass` = ? WHERE `id` = ?";
$db_results = Dba::query($sql, array($data['host'], $data['port'], $data['name'], $data['user'], $data['pass'], $uid));
return true;
} // update_instance
/**
* instance_fields
* This returns a key'd array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the
* fields so that we can on-the-fly generate a form
*/
public function instance_fields() {
$fields['name'] = array('description' => T_('Instance Name'),'type'=>'textbox');
$fields['host'] = array('description' => T_('Hostname'),'type'=>'textbox');
$fields['port'] = array('description' => T_('Port'),'type'=>'textbox');
$fields['user'] = array('description' => T_('Username'),'type'=>'textbox');
$fields['pass'] = array('description' => T_('Password'),'type'=>'textbox');
return $fields;
} // instance_fields
/**
* get_instance
* This returns a single instance and all it's variables
*/
public function get_instance($instance='') {
$instance = $instance ? $instance : Config::get('xbmc_active');
$sql = "SELECT * FROM `localplay_xbmc` WHERE `id` = ?";
$db_results = Dba::query($sql, array($instance));
$row = Dba::fetch_assoc($db_results);
return $row;
} // get_instance
/**
* set_active_instance
* This sets the specified instance as the 'active' one
*/
public function set_active_instance($uid,$user_id='') {
// Not an admin? bubkiss!
if (!$GLOBALS['user']->has_access('100')) {
$user_id = $GLOBALS['user']->id;
}
$user_id = $user_id ? $user_id : $GLOBALS['user']->id;
Preference::update('xbmc_active', $user_id, intval($uid));
Config::set('xbmc_active', intval($uid), true);
return true;
} // set_active_instance
/**
* get_active_instance
* This returns the UID of the current active instance
* false if none are active
*/
public function get_active_instance() {
} // get_active_instance
public function add_url(Stream_URL $url) {
try {
$this->_xbmc->Playlist->Add(array(
'playlistid' => $this->_playlistId,
'item' => array('file' => $url->url)
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'add_url failed: ' . $ex->getMessage(), 1);
return false;
}
}
/**
* delete_track
* Delete a track from the xbmc playlist
*/
public function delete_track($track) {
try {
$this->_xbmc->Playlist->Remove(array(
'playlistid' => $this->_playlistId,
'position' => $track
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'delete_track failed: ' . $ex->getMessage(), 1);
return false;
}
} // delete_track
/**
* clear_playlist
* This deletes the entire xbmc playlist.
*/
public function clear_playlist() {
try {
$this->_xbmc->Playlist->Clear(array(
'playlistid' => $this->_playlistId
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'clear_playlist failed: ' . $ex->getMessage(), 1);
return false;
}
} // clear_playlist
/**
* play
* This just tells xbmc to start playing, it does not
* take any arguments
*/
public function play() {
try {
// XBMC requires to load a playlist to play. We don't know if this play is after a new playlist or after pause
// So we get current status
$status = $this->status();
if ($status['state'] == 'stop') {
$this->_xbmc->Player->Open(array(
'item' => array('playlistid' => $this->_playlistId))
);
} else {
$this->_xbmc->Player->PlayPause(array(
'playerid' => $this->_playlistId,
'play' => true)
);
}
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'play failed: ' . $ex->getMessage(), 1);
return false;
}
} // play
/**
* pause
* This tells xbmc to pause the current song
*/
public function pause() {
try {
$this->_xbmc->Player->PlayPause(array(
'playerid' => $this->_playerId,
'play' => false)
);
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'pause failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // pause
/**
* stop
* This just tells xbmc to stop playing, it does not take
* any arguments
*/
public function stop() {
try {
$this->_xbmc->Player->Stop(array(
'playerid' => $this->_playerId
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'stop failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // stop
/**
* skip
* This tells xbmc to skip to the specified song
*/
public function skip($song) {
try {
$this->_xbmc->Player->GoTo(array(
'playerid' => $this->_playerId,
'to' => $song
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'skip failed, is the player started?: ' . $ex->getMessage(), 1);
return false;
}
} // skip
/**
* This tells xbmc to increase the volume
*/
public function volume_up() {
try {
$this->_xbmc->Application->SetVolume(array(
'volume' => 'increment'
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'volume_up failed: ' . $ex->getMessage(), 1);
return false;
}
} // volume_up
/**
* This tells xbmc to decrease the volume
*/
public function volume_down() {
try {
$this->_xbmc->Application->SetVolume(array(
'volume' => 'decrement'
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'volume_down failed: ' . $ex->getMessage(), 1);
return false;
}
} // volume_down
/**
* next
* This just tells xbmc to skip to the next song
*/
public function next() {
try {
$this->_xbmc->Player->GoTo(array(
'playerid' => $this->_playerId,
'to' => 'next'
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'next failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // next
/**
* prev
* This just tells xbmc to skip to the prev song
*/
public function prev() {
try {
$this->_xbmc->Player->GoTo(array(
'playerid' => $this->_playerId,
'to' => 'previous'
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'prev failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // prev
/**
* volume
* This tells xbmc to set the volume to the specified amount
*/
public function volume($volume) {
try {
$this->_xbmc->Application->SetVolume(array(
'volume' => $volume
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'volume failed: ' . $ex->getMessage(), 1);
return false;
}
} // volume
/**
* repeat
* This tells xbmc to set the repeating the playlist (i.e. loop) to either on or off
*/
public function repeat($state) {
try {
$this->_xbmc->Player->SetRepeat(array(
'playerid' => $this->_playerId,
'repeat' => ($state ? 'all' : 'off')
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'repeat failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // repeat
/**
* random
* This tells xbmc to turn on or off the playing of songs from the playlist in random order
*/
public function random($onoff) {
try {
$this->_xbmc->Player->SetShuffle(array(
'playerid' => $this->_playerId,
'shuffle' => $onoff
));
return true;
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'random failed, is the player started? ' . $ex->getMessage(), 1);
return false;
}
} // random
/**
* get
* This functions returns an array containing information about
* The songs that xbmc currently has in it's playlist. This must be
* done in a standardized fashion
*/
public function get() {
$results = array();
try {
$playlist = $this->_xbmc->Playlist->GetItems(array(
'playlistid' => $this->_playlistId,
'properties' => array('file')
));
for ($i = $playlist['limits']['start']; $i < $playlist['limits']['end']; ++$i) {
$item = $playlist['items'][$i];
$data = array();
$data['link'] = $item['file'];
$data['id'] = $i;
$data['track'] = $i + 1;
$url_data = $this->parse_url($data['link']);
if ($url_data != null) {
$song = new Song($url_data['oid']);
if ($song != null) {
$data['name'] = $song->get_artist_name() . ' - ' . $song->title;
}
}
if (!$data['name']) {
$data['name'] = $item['label'];
}
$results[] = $data;
}
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'get failed: ' . $ex->getMessage(), 1);
}
return $results;
} // get
/**
* status
* This returns bool/int values for features, loop, repeat and any other features
* that this localplay method supports.
* This works as in requesting the xbmc properties
*/
public function status() {
$array = array();
try {
$appprop = $this->_xbmc->Application->GetProperties(array(
'properties' => array('volume')
));
$array['volume'] = intval($appprop['volume']);
try {
$currentplay = $this->_xbmc->Player->GetItem(array(
'playerid' => $this->_playerId,
'properties' => array('file')
));
// We assume it's playing. No pause detection support.
$array['state'] = 'play';
$playprop = $this->_xbmc->Player->GetProperties(array(
'playerid' => $this->_playerId,
'properties' => array('repeat', 'shuffled')
));
$array['repeat'] = ($playprop['repeat'] != "off");
$array['random'] = (strtolower($playprop['shuffled']) == 1) ;
$array['track'] = $currentplay['file'];
$url_data = $this->parse_url($array['track']);
$song = new Song($url_data['oid']);
if ($song->title || $song->get_artist_name() || $song->get_album_name()) {
$array['track_title'] = $song->title;
$array['track_artist'] = $song->get_artist_name();
$array['track_album'] = $song->get_album_name();
}
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'get current item failed, player probably stopped. ' . $ex->getMessage(), 1);
$array['state'] = 'stop';
}
} catch (XBMC_RPC_Exception $ex) {
debug_event('xbmc', 'status failed: ' . $ex->getMessage(), 1);
}
return $array;
} // status
/**
* connect
* This functions creates the connection to xbmc and returns
* a boolean value for the status, to save time this handle
* is stored in this class
*/
public function connect() {
$options = self::get_instance();
try {
$this->_xbmc = new XBMC_RPC_TCPClient($options);
return true;
} catch (XBMC_RPC_ConnectionException $ex) {
debug_event('xbmc', 'xbmc connection failed: ' . $ex->getMessage(), 1);
return false;
}
} // connect
} //end of AmpacheXbmc
?>

View file

@ -0,0 +1,327 @@
<?php
require_once 'ClientException.php';
require_once 'ConnectionException.php';
require_once 'RequestException.php';
require_once 'ResponseException.php';
require_once 'Server.php';
require_once 'Namespace.php';
require_once 'Response.php';
abstract class XBMC_RPC_Client {
/**
* @var XBMC_RPC_Server A server object instance representing the server
* to be used for remote procedure calls.
* @access protected
*/
protected $server;
/**
* @var XBMC_RPC_Namespace The root namespace instance.
* @access private
*/
private $rootNamespace;
/**
* @var bool A flag to indicate if the JSON-RPC version is legacy, ie before
* the XBMC Eden updates. This can be used to determine the format of commands
* to be used with this library, allowing client code to support legacy systems.
* @access private
*/
private $isLegacy = false;
/**
* Constructor.
*
* Connects to the server and populates a list of available commands by
* having the server introspect.
*
* @param mixed $parameters An associative array of connection parameters,
* or a valid connection URI as a string. If supplying an array, the following
* paramters are accepted: host, port, user and pass. Any other parameters
* are discarded.
* @exception XBMC_RPC_ConnectionException if it is not possible to connect to
* the server.
* @access public
*/
public function __construct($parameters) {
try {
$server = new XBMC_RPC_Server($parameters);
} catch (XBMC_RPC_ServerException $e) {
throw new XBMC_RPC_ConnectionException($e->getMessage(), $e->getCode(), $e);
}
$this->server = $server;
$this->prepareConnection();
$this->assertCanConnect();
$this->createRootNamespace();
}
/**
* Delegates any direct Command calls to the root namespace.
*
* @param string $name The name of the called command.
* @param mixed $arguments An array of arguments used to call the command.
* @return The result of the command call as returned from the namespace.
* @exception XBMC_RPC_InvalidCommandException if the called command does not
* exist in the root namespace.
* @access public
*/
public function __call($name, array $arguments) {
return call_user_func_array(array($this->rootNamespace, $name), $arguments);
}
/**
* Delegates namespace accesses to the root namespace.
*
* @param string $name The name of the requested namespace.
* @return XBMC_RPC_Namespace The requested namespace.
* @exception XBMC_RPC_InvalidNamespaceException if the namespace does not
* exist in the root namespace.
* @access public
*/
public function __get($name) {
return $this->rootNamespace->$name;
}
/**
* Executes a remote procedure call using the supplied XBMC_RPC_Command
* object.
*
* @param XBMC_RPC_Command The command to execute.
* @return XBMC_RPC_Response The response from the remote procedure call.
* @access public
*/
public function executeCommand(XBMC_RPC_Command $command) {
return $this->sendRpc($command->getFullName(), $command->getArguments());
}
/**
* Determines if the XBMC system to which the client is connected is legacy
* (pre Eden) or not. This is useful because the format of commands/params
* is different in the Eden RPC implementation.
*
* @return bool True if the system is legacy, false if not.
* @access public
*/
public function isLegacy() {
return $this->isLegacy;
}
/**
* Asserts that the server is reachable and a connection can be made.
*
* @return void
* @exception XBMC_RPC_ConnectionException if it is not possible to connect to
* the server.
* @abstract
* @access protected
*/
protected abstract function assertCanConnect();
/**
* Prepares for a connection to XBMC.
*
* Should be used by child classes for any pre-connection logic which is necessary.
*
* @return void
* @exception XBMC_RPC_ClientException if it was not possible to prepare for
* connection successfully.
* @abstract
* @access protected
*/
protected abstract function prepareConnection();
/**
* Sends a JSON-RPC request to XBMC and returns the result.
*
* @param string $json A JSON-encoded string representing the remote procedure call.
* This string should conform to the JSON-RPC 2.0 specification.
* @param string $rpcId The unique ID of the remote procedure call.
* @return string The JSON-encoded response string from the server.
* @exception XBMC_RPC_RequestException if it was not possible to make the request.
* @access protected
* @link http://groups.google.com/group/json-rpc/web/json-rpc-2-0 JSON-RPC 2.0 specification
*/
protected abstract function sendRequest($json, $rpcId);
/**
* Build a JSON-RPC 2.0 compatable json_encoded string representing the
* specified command, parameters and request id.
*
* @param string $command The name of the command to be called.
* @param mixed $params An array of paramters to be passed to the command.
* @param string $rpcId A unique string used for identifying the request.
* @access private
*/
private function buildJson($command, $params, $rpcId) {
$data = array(
'jsonrpc' => '2.0',
'method' => $command,
'params' => $params,
'id' => $rpcId
);
return json_encode($data);
}
/**
* Ensures that the recieved response from a remote procedure call is valid.
*
* $param XBMC_RPC_Response $response A response object encapsulating remote
* procedure call response data as returned from Client::sendRequest().
* @return bool True of the reponse is valid, false if not.
* @access private
*/
private function checkResponse(XBMC_RPC_Response $response, $rpcId) {
return ($response->getId() == $rpcId);
}
/**
* Creates the root namespace instance.
*
* @return void
* @access private
*/
private function createRootNamespace() {
$commands = $this->loadAvailableCommands();
$this->rootNamespace = new XBMC_RPC_Namespace('root', $commands, $this);
}
/**
* Generates a unique string to be used as a remote procedure call ID.
*
* @return string A unique string.
* @access private
*/
private function getRpcId() {
return uniqid();
}
/**
* Retrieves an array of commands by requesting the RPC server to introspect.
*
* @return mixed An array of available commands which may be executed on the server.
* @exception XBMC_RPC_RequestException if it is not possible to retrieve a list of
* available commands.
* @access private
*/
private function loadAvailableCommands() {
try {
$response = $this->sendRpc('JSONRPC.Introspect');
} catch (XBMC_RPC_Exception $e) {
throw new XBMC_RPC_RequestException(
'Unable to retrieve list of available commands: ' . $e->getMessage()
);
}
if (isset($response['commands'])) {
$this->isLegacy = true;
return $this->loadAvailableCommandsLegacy($response);
}
$commands = array();
foreach (array_keys($response['methods']) as $command) {
$array = $this->commandStringToArray($command);
$commands = $this->mergeCommandArrays($commands, $array);
}
return $commands;
}
/**
* Retrieves an array of commands by requesting the RPC server to introspect.
*
* This method supports the legacy implementation of XBMC's RPC.
*
* @return mixed An array of available commands which may be executed on the server.
* @access private
*/
private function loadAvailableCommandsLegacy($response) {
$commands = array();
foreach ($response['commands'] as $command) {
$array = $this->commandStringToArray($command['command']);
$commands = $this->mergeCommandArrays($commands, $array);
}
return $commands;
}
/**
* Converts a dot-delimited command name to a multidimensional array format.
*
* @return mixed An array representing the command.
* @access private
*/
private function commandStringToArray($command) {
$path = explode('.', $command);
if (count($path) === 1) {
$commands[] = $path[0];
continue;
}
$command = array_pop($path);
$array = array();
$reference =& $array;
foreach ($path as $i => $key) {
if (is_numeric($key) && intval($key) > 0 || $key === '0') {
$key = intval($key);
}
if ($i === count($path) - 1) {
$reference[$key] = array($command);
} else {
if (!isset($reference[$key])) {
$reference[$key] = array();
}
$reference =& $reference[$key];
}
}
return $array;
}
/**
* Recursively merges the supplied arrays whilst ensuring that commands are
* not duplicated within a namespace.
*
* Note that array_merge_recursive is not suitable here as it does not ensure
* that values are distinct within an array.
*
* @param mixed $base The base array into which $append will be merged.
* @param mixed $append The array to merge into $base.
* @return mixed The merged array of commands and namespaces.
* @access private
*/
private function mergeCommandArrays(array $base, array $append) {
foreach ($append as $key => $value) {
if (!array_key_exists($key, $base) && !is_numeric($key)) {
$base[$key] = $append[$key];
continue;
}
if (is_array($value) || is_array($base[$key])) {
$base[$key] = $this->mergeCommandArrays($base[$key], $append[$key]);
} elseif (is_numeric($key)) {
if (!in_array($value, $base)) {
$base[] = $value;
}
} else {
$base[$key] = $value;
}
}
return $base;
}
/**
* Executes a remote procedure call using the supplied command name and parameters.
*
* @param string $command The full, dot-delimited name of the command to call.
* @param mixed $params An array of parameters to be passed to the called method.
* @return mixed The data returned from the response.
* @exception XBMC_RPC_RequestException if it was not possible to make the request.
* @exception XBMC_RPC_ResponseException if the response was not being properly received.
* @access private
*/
private function sendRpc($command, $params = array()) {
$rpcId = $this->getRpcId();
$json = $this->buildJson($command, $params, $rpcId);
$response = new XBMC_RPC_Response($this->sendRequest($json, $rpcId));
if (!$this->checkResponse($response, $rpcId)) {
throw new XBMC_RPC_ResponseException('JSON RPC request/response ID mismatch');
}
return $response->getData();
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_ClientException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,86 @@
<?php
/**
* A JSON-RPC command.
*/
class XBMC_RPC_Command {
/**
* @var string The name of the command.
* @access private
*/
private $name;
/**
* @var The namespace to which the instance belongs.
* @access private
*/
private $parentNamespace;
/**
* @var The client to be used for executing the command.
* @access private
*/
private $client;
/**
* @var An array of arguments to be passed with the command.
* @access private
*/
private $arguments = array();
/**
* Constructor.
*
* @param string $name The name of the desired command.
* @param XBMC_RPC_Client $client The client instance responsible for operating
* the Command instance.
* @param mixed $parent The parent XBMC_RPC_Command object, or null if there is
* no parent of this instance.
* @access public
*/
public function __construct($name, XBMC_RPC_Client $client, XBMC_RPC_Namespace $parent) {
$this->name = $name;
$this->parentNamespace = $parent;
$this->client = $client;
}
/**
* Executes the remote procedure call command.
*
* @param mixed $arguments An array of arguments to be passed along with the
* command.
* @return mixed The response data as returned from XBMC_RPC_Client::sendRpc().
* @exception XBMC_RPC_Exception if the remote procedure call could be carried
* out successfully.
* @access public
*/
public function execute(array $arguments = array()) {
if (count($arguments) == 1) {
$arguments = array_shift($arguments);
}
$this->arguments = $arguments;
return $this->client->executeCommand($this);
}
/**
* Gets an array of arguments which accompany the command.
*
* @return mixed The array of argument which accompany this command.
* @access public
*/
public function getArguments() {
return $this->arguments;
}
/**
* Gets the full, dot-delimited name of the command including its namespace path.
*
* @return string The command name.
* @access public
*/
public function getFullName() {
return $this->parentNamespace->getFullName() . '.' . $this->name;
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_CommandException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_ConnectionException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,5 @@
<?php
class XBMC_RPC_Exception extends Exception {
}

View file

@ -0,0 +1,136 @@
<?php
require_once 'Client.php';
class XBMC_RPC_HTTPClient extends XBMC_RPC_Client {
/**
* @var string The URI of the XBMC server to which requests will be made.
*/
private $uri;
/**
* @var resource The curl resource used for making requests.
*/
private $curlResource;
/**
* @var int The amount of time for which the script will try to connect before giving up.
*/
private $timeout = 15;
/**
* Destructor.
*
* Cleans up the curl resource if necessary.
*/
public function __destruct() {
if (is_resource($this->curlResource)) {
curl_close($this->curlResource);
}
}
/**
* Sets the number of seconds the script will wait while trying to connect
* to the server before giving up.
*
* @param int $seconds The number of seconds to wait.
* @return void
*/
public function setTimeout($seconds) {
$this->timeout = (int) $seconds;
if ($this->timeout < 0) {
$this->timeout = 0;
}
}
/**
* Asserts that the server is reachable and a connection can be made.
*
* @return void
* @exception XBMC_RPC_ConnectionException if it is not possible to connect to
* the server.
*/
protected function assertCanConnect() {
if (extension_loaded('curl')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $this->uri);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
if (!curl_exec($ch) || !in_array(curl_getinfo($ch, CURLINFO_HTTP_CODE), array('200', '401'))) {
throw new XBMC_RPC_ConnectionException('Unable to connect to XBMC server via HTTP');
}
} else {
throw new XBMC_RPC_ConnectionException('Cannot test if XBMC server is reachable via HTTP because cURL is not installed');
}
}
/**
* Prepares for a connection to XBMC via HTTP.
*
* @exception XBMC_RPC_ClientException if it was not possible to prepare for
* connection successfully.
* @access protected
*/
protected function prepareConnection() {
if (!$uri = $this->buildUri()) {
throw new XBMC_RPC_ClientException('Unable to parse server parameters into valid URI string');
}
$this->uri = $uri;
}
/**
* Sends a JSON-RPC request to XBMC and returns the result.
*
* @param string $json A JSON-encoded string representing the remote procedure call.
* This string should conform to the JSON-RPC 2.0 specification.
* @param string $rpcId The unique ID of the remote procedure call.
* @return string The JSON-encoded response string from the server.
* @exception XBMC_RPC_RequestException if it was not possible to make the request.
* @access protected
* @link http://groups.google.com/group/json-rpc/web/json-rpc-2-0 JSON-RPC 2.0 specification
*/
protected function sendRequest($json, $rpcId) {
if (empty($this->curlResource)) {
$this->curlResource = $this->createCurlResource();
}
curl_setopt($this->curlResource, CURLOPT_POSTFIELDS, $json);
curl_setopt($this->curlResource, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
if (!$response = curl_exec($this->curlResource)) {
throw new XBMC_RPC_RequestException('Could not make a request the server');
}
return $response;
}
/**
* Builds the server URI from the supplied parameters.
*
* @return string The server URI.
* @access private
*/
private function buildUri() {
$parameters = $this->server->getParameters();
$credentials = '';
if (!empty($parameters['user'])) {
$credentials = $parameters['user'];
$credentials .= empty($parameters['pass']) ? '@' : ':' . $parameters['pass'] . '@';
}
return sprintf('http://%s%s:%d/jsonrpc', $credentials, $parameters['host'], $parameters['port']);
}
/**
* Creates a curl resource with the correct settings for making JSON-RPC calls
* to XBMC.
*
* @return resource A new curl resource.
* @access private
*/
private function createCurlResource() {
$curlResource = curl_init();
curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curlResource, CURLOPT_POST, 1);
curl_setopt($curlResource, CURLOPT_URL, $this->uri);
return $curlResource;
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_InvalidCommandException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_InvalidNamespaceException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,166 @@
<?php
require_once 'InvalidNamespaceException.php';
require_once 'InvalidCommandException.php';
require_once 'Command.php';
/**
* A collection of commands and namespaces.
*/
class XBMC_RPC_Namespace {
/**
* @var string The name of the namespace.
* @access private
*/
private $name;
/**
* @var XBMC_RPC_Command The parent namespace of the current instance.
* @access private
*/
private $parentNamespace;
/**
* @var The tree of commands and namespaces which are children of the current
* instance.
* @access private
*/
private $children = array();
/**
* @var A cache of child XBMC_RPC_Command and XBMC_RPC_Namespace objects.
* @access private
*/
private $objectCache = array();
/**
* @var An instance of XBMC_RPC_Client to which this XBMC_RPC_Namespace
* instance belongs.
* @access private
*/
private $client;
/**
* Constructor.
*
* @param string $name The name of the namespace.
* @param mixed $children An array of commands and namespaces which are
* children of the current instance.
* @param XBMC_RPC_Client $client The client instance to which this instance
* belongs.
* @param mixed $parent The parent XBMC_RPC_Namespace object, or null if this
* instance is the root namespace.
* @access public
*/
public function __construct($name, array $children, XBMC_RPC_Client $client, XBMC_RPC_Namespace $parent = null) {
$this->name = $name;
$this->children = $children;
$this->client = $client;
$this->parentNamespace = $parent;
}
/**
* Executes the called command.
*
* @param string $name The name of the command to call.
* @arguments mixed An array of arguments to be send with the remote
* procedure call.
* @return XBMC_RPC_Response The response of the remote procedure call.
* @exception XBMC_RPC_InvalidCommandException if the requested command does
* not exist in this namespace.
* @access public
*/
public function __call($name, array $arguments) {
$this->assertHasChildCommand($name);
if (empty($this->objectCache[$name])) {
$this->objectCache[$name] = new XBMC_RPC_Command($name, $this->client, $this);
}
return $this->objectCache[$name]->execute($arguments);
}
/**
* Gets the requested child namespace.
*
* @param string $name The name of the namespace to get.
* @return XBMC_RPC_Namespace The requested child namespace.
* @exception XBMC_RPC_InvalidNamespaceException if the requested namespace does
* not exist in this namespace.
* @access public
*/
public function __get($name) {
$this->assertHasChildNamespace($name);
if (empty($this->objectCache[$name])) {
$this->objectCache[$name] = new XBMC_RPC_Namespace($name, $this->children[$name], $this->client, $this);
}
return $this->objectCache[$name];
}
/**
* Gets the full dot-delimited string representing the path from the root
* namespace to the current namespace.
*
* @return string The dot-delimited string.
* @access public
*/
public function getFullName() {
$name = '';
if (!empty($this->parentNamespace)) {
$name = $this->parentNamespace->getFullName() . '.' . $this->name;
}
return trim($name, '.');
}
/**
* Asserts that the the namespace contains the specified command as a direct
* child.
*
* @param string $name The name of the command to check for.
* @exception XBMC_RPC_InvalidCommandException if the command is not a direct
* child of this namespace.
* @access private
*/
private function assertHasChildCommand($name) {
if (!$this->hasChildCommand($name)) {
throw new XBMC_RPC_InvalidCommandException("Command $name does not exist in namespace $this->name");
}
}
/**
* Asserts that the the namespace contains the specified namespace as a direct
* child.
*
* @param string $name The name of the namespace to check for.
* @exception XBMC_RPC_InvalidNamespaceException if the namespace is not a direct
* child of this namespace.
* @access private
*/
private function assertHasChildNamespace($name) {
if (!$this->hasChildNamespace($name)) {
throw new XBMC_RPC_InvalidNamespaceException("Namespace $name does not exist in namespace $this->name");
}
}
/**
* Checks if the namespace has the specified command as a direct child.
*
* @param string $name The name of the command to check for.
* @return bool True if the command exists in this namespace, false if not.
* @access private
*/
private function hasChildCommand($name) {
return in_array($name, $this->children);
}
/**
* Checks if the namespace has the specified namespace as a direct child.
*
* @param string $name The name of the namespace to check for.
* @return bool True if the namespace exists in this namespace, false if not.
* @access private
*/
private function hasChildNamespace($name) {
return array_key_exists($name, $this->children);
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_RequestException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,66 @@
<?php
class XBMC_RPC_Response {
/**
* @var string The id of the response as returned from the server.
* @access private
*/
private $id;
/**
* @var string The data returned from the server if the response was successful.
* @access private
*/
private $data;
/**
* Constructor.
*
* @param string $response The JSON-decoded response string
* as returned from the server
*/
public function __construct($response) {
$response = $this->decodeResponse($response);
$this->id = $response['id'];
if (isset($response['error'])) {
throw new XBMC_RPC_ResponseException($response['error']['message'], $response['error']['code']);
} elseif (!isset($response['result'])) {
throw new XBMC_RPC_ResponseException('Invalid JSON RPC response');
}
$this->data = $response['result'];
}
/**
* Gets the response data.
*
* @return mixed The response data.
*/
public function getData() {
return $this->data;
}
/**
* Gets the response id.
*
* @return string The response id.
*/
public function getId() {
return $this->id;
}
/**
* Takes a JSON string as returned from the server and decodes it into an
* associative array.
*/
private function decodeResponse($json) {
if (extension_loaded('mbstring')) {
$encoding = mb_detect_encoding($json, 'ASCII,UTF-8,ISO-8859-1,windows-1252,iso-8859-15');
if ($encoding && !in_array($encoding, array('UTF-8', 'ASCII'))) {
$json = mb_convert_encoding($json, 'UTF-8', $encoding);
}
}
return json_decode($json, true);
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_ResponseException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,88 @@
<?php
require_once 'ServerException.php';
class XBMC_RPC_Server {
/**
* @var mixed An array of parameters used to connect to the server.
* @access private
*/
private $parameters = array();
/**
* Constructor.
*
* @param mixed $parameters An associative array of connection parameters,
* or a valid connection URI as a string. If supplying an array, the following
* paramters are accepted: host, port, user and pass. Any other parameters
* are discarded.
* @exception XBMC_RPC_ServerException if the supplied parameters could not
* be parsed successfully.
* @access public
*/
public function __construct($parameters) {
if (!$parameters = $this->parseParameters($parameters)) {
throw new XBMC_RPC_ServerException('Unable to parse server parameters');
}
$this->parameters = $parameters;
}
/**
* Checks if the server is connected.
*
* @return bool True if the server is connected, false if not.
* @access public
*/
public function isConnected() {
return $this->connected;
}
/**
* Gets the connection parameters/
*
* @return mixed The connection parameters as an associative array.
* @access public
*/
public function getParameters() {
return $this->parameters;
}
/**
* Parses the supplied parameters into a standard associative array format.
*
* @param mixed $parameters An associative array of connection parameters,
* or a valid connection URI as a string. If supplying an array, the following
* paramters are accepted: host, port, user and pass. Any other parameters
* are discarded.
* @return mixed The connection parameters as an associative array, or false
* if the parameters could not be parsed. The array will have the following
* keys: host, port, user and pass.
* @access private
*/
private function parseParameters($parameters) {
if (is_string($parameters)) {
$parameters = preg_replace('#^[a-z]+://#i', '', trim($parameters));
if (!$parameters = parse_url('http://' . $parameters)) {
return false;
}
}
if (!is_array($parameters)) {
// if parameters are not a string or an array, something is wrong
return false;
}
$defaults = array(
'host' => 'localhost',
'port' => 8080,
'user' => null,
'pass' => null
);
$parameters = array_intersect_key(array_merge($defaults, $parameters), $defaults);
return $parameters;
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once 'Exception.php';
class XBMC_RPC_ServerException extends XBMC_RPC_Exception {
}

View file

@ -0,0 +1,139 @@
<?php
require_once 'Client.php';
class XBMC_RPC_TCPClient extends XBMC_RPC_Client {
/**
* @var resource A file pointer resource for reading over the connected socket.
* @access private
*/
private $fp;
/**
* Destructor.
*
* Cleans up the file resource if necessary.
*/
public function __destruct() {
if (is_resource($this->fp)) {
fclose($this->fp);
}
}
/**
* Asserts that the server is reachable and a connection can be made.
*
* @return void
* @exception XBMC_RPC_ConnectionException if it is not possible to connect to
* the server.
* @access protected
*/
protected function assertCanConnect() {
if (!$this->canConnect()) {
throw new XBMC_RPC_ConnectionException('Unable to connect to XBMC server via TCP');
}
}
/**
* Prepares for a connection to XBMC via TCP.
*
* @return void
* @access protected
*/
protected function prepareConnection() {
$parameters = $this->server->getParameters();
$this->fp = @fsockopen($parameters['host'], $parameters['port']);
}
/**
* Sends a JSON-RPC request to XBMC and returns the result.
*
* @param string $json A JSON-encoded string representing the remote procedure call.
* This string should conform to the JSON-RPC 2.0 specification.
* @param string $rpcId The unique ID of the remote procedure call.
* @return string The JSON-encoded response string from the server.
* @exception XBMC_RPC_RequestException if it was not possible to make the request.
* @access protected
* @link http://groups.google.com/group/json-rpc/web/json-rpc-2-0 JSON-RPC 2.0 specification
*/
protected function sendRequest($json, $rpcId) {
$this->prepareConnection();
if (!$this->canConnect()) {
throw new XBMC_RPC_ConnectionException('Lost connection to XBMC server');
}
fwrite($this->fp, $json);
while (true) {
$result = $this->readJsonObject();
if (strpos($result, '"id" : "' . $rpcId . '"') !== false) {
break;
}
if (strpos($result, '"id":"' . $rpcId . '"') !== false) {
break;
}
}
fclose($this->fp);
return $result;
}
/**
* Checks if it is possible to connect to the server.
*
* @return bool True if it is possible to connect, false if not.
* @access private
*/
private function canConnect() {
return is_resource($this->fp);
}
/**
* Reads a single JSON object from the socket and returns it.
*
* @return string The JSON object string from the server.
* @access private
*/
private function readJsonObject() {
$open = $close = 0;
$escaping = false;
$quoteChar = null;
$result = '';
while (false !== ($char = fgetc($this->fp))) {
if (!$escaping) {
switch ($char) {
case "'":
case '"':
if (null === $quoteChar) {
$quoteChar = $char;
} elseif ($quoteChar == $char) {
$quoteChar = null;
}
break;
case '{':
if (null === $quoteChar) {
++$open;
}
break;
case '}':
if (null === $quoteChar) {
++$close;
}
break;
case '\\':
$escaping = true;
break;
}
} else {
$escaping = false;
}
$result .= $char;
if ($open == $close) {
break;
}
}
return $result;
}
}