# HG changeset patch # User Dan # Date 1206318273 14400 # Node ID 860ba7141641b006431ca97be3c3cd5278aaaa37 # Parent cddc2ba706d67f36aeea866c98de39dc705fc251 Should be nearly finished now - includes volume control, length measurement, and seems pretty stable diff -r cddc2ba706d6 -r 860ba7141641 ajax.php --- a/ajax.php Sun Mar 23 15:24:06 2008 -0400 +++ b/ajax.php Sun Mar 23 20:24:33 2008 -0400 @@ -9,5 +9,104 @@ * This script is in the public domain. Use it for good, not evil. */ +status('initializing Services_JSON'); +$json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); +// keep track of playlist refresh +$playlist_last_refresh = time(); +/** + * Terminate a request with an error string formatted as JSON. + * @param string Error message + */ + +function json_die($msg) +{ + global $json; + echo $json->encode(array( + 'mode' => 'error', + 'error' => $msg + )); + return true; +} + +function ajax_request_handler($httpd) +{ + global $playlist, $mime_types, $json; + + // Set content type + $httpd->header("Content-type: {$mime_types['js']}"); + + // get PATH_INFO + $pathinfo = @substr(@substr($_SERVER['REQUEST_URI'], 1), @strpos(@substr($_SERVER['REQUEST_URI'], 1), '/')+1); + if ( empty($pathinfo) ) + { + return json_die('No action specified on URI'); + } + + $params = explode('/', $pathinfo); + $action =& $params[0]; + switch ( $action ) + { + case 'stop': + case 'next': + case 'prev': + echo dcop_action('player', 'stop'); + break; + case 'play': + echo dcop_action('player', 'playPause'); + break; + case 'jump': + $tid =& $params[1]; + if ( !preg_match('/^[0-9]+$/', $tid) ) + { + return die_json('Invalid track ID'); + } + $tid = intval($tid); + dcop_action('playlist', "playByIndex $tid"); + $return = array( + 'current_track_length' => $playlist[$tid]['length_int'], + 'current_track_pos' => 0 + ); + echo $json->encode($return); + break; + case 'volume': + $volume =& $params[1]; + if ( !preg_match('/^[0-9]+$/', $volume) ) + { + return die_json('Invalid track ID'); + } + $volume = intval($volume); + dcop_action('player', "setVolume $volume"); + $return = array( + 'volume' => $volume + ); + echo $json->encode($return); + break; + case 'refresh': + global $playlist_last_refresh, $playlist, $playlist_last_md5; + if ( $playlist_last_refresh + 60 < time() ) + { + rebuild_playlist(); + } + $current_track = dcop_action('playlist', 'getActiveIndex'); + $return = array( + 'is_playing' => dcop_action('player', 'isPlaying'), + 'current_track' => $current_track, + 'volume' => dcop_action('player', 'getVolume'), + // include the MD5 of the playlist so that if it changes, the + // client can refresh (otherwise things get madly corrupted) + 'playlist_hash' => $playlist_last_md5 + ); + if ( isset($playlist[$current_track]) ) + { + $return['current_track_length'] = $playlist[$current_track]['length_int']; + $return['current_track_pos'] = dcop_action('player', 'trackCurrentTime'); + } + echo $json->encode($return); + break; + default: + return json_die("Undefined action: $action"); + } +} + diff -r cddc2ba706d6 -r 860ba7141641 functions.php --- a/functions.php Sun Mar 23 15:24:06 2008 -0400 +++ b/functions.php Sun Mar 23 20:24:33 2008 -0400 @@ -68,12 +68,21 @@ * Rebuilds the copy of the playlist in RAM */ +$playlist_last_md5 = ''; + function rebuild_playlist() { // import what we need global $homedir, $playlist; // sync and load the playlist file $playlist_file = dcop_action('playlist', 'saveCurrentPlaylist'); + // check MD5 - if it's not changed, exit to save CPU cycles + global $playlist_last_md5; + if ( $playlist_last_md5 == @md5_file($playlist_file) ) + { + return true; + } + $playlist_last_md5 = @md5_file($playlist_file); // start XML parser try { @@ -97,7 +106,8 @@ 'title' => strval($child->Title), 'artist' => strval($child->Artist), 'album' => strval($child->Album), - 'length' => seconds_to_str(intval($child->Length)) + 'length' => seconds_to_str(intval($child->Length)), + 'length_int' => intval($child->Length) ); $playlist[] = $item; } diff -r cddc2ba706d6 -r 860ba7141641 json.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/json.php Sun Mar 23 20:24:33 2008 -0400 @@ -0,0 +1,805 @@ + +* @author Matt Knapp +* @author Brett Stimmerman +* @copyright 2005 Michal Migurski +* @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ +* @license http://www.opensource.org/licenses/bsd-license.php +* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 +*/ + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_SLICE', 1); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_STR', 2); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_ARR', 3); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_OBJ', 4); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_CMT', 5); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * @example + // create a new instance of Services_JSON + $json = new Services_JSON(); + + // convert a complexe value to JSON notation, and send it to the browser + $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + $output = $json->encode($value); + + print($output); + // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + + // accept incoming POST data, assumed to be in JSON notation + $input = file_get_contents('php://input', 1000000); + $value = $json->decode($input); + + * + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var) + { + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + } + + // treat it like a regular array + $elements = array_map(array($this, 'encode'), $var); + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = get_object_vars($var); + + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode(strval($name)) . ':' . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { + // found a quote, we're in a string, and it's not escaped + // we know that it's not escaped becase there is _not_ an + // odd number of backslashes at the end of the string so far + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> \ No newline at end of file diff -r cddc2ba706d6 -r 860ba7141641 playlist.php --- a/playlist.php Sun Mar 23 15:24:06 2008 -0400 +++ b/playlist.php Sun Mar 23 20:24:33 2008 -0400 @@ -19,6 +19,8 @@ $smarty->assign('active', $active); $smarty->assign('scripts', array( 'ajax.js', + 'domutils.js', + 'volume.js' )); $smarty->display('playlist.tpl'); } diff -r cddc2ba706d6 -r 860ba7141641 scripts/ajax.js --- a/scripts/ajax.js Sun Mar 23 15:24:06 2008 -0400 +++ b/scripts/ajax.js Sun Mar 23 20:24:33 2008 -0400 @@ -1,1 +1,244 @@ -// Hello world +/** + * AJAX functions + * + * Web control interface script for Amarok + * Written by Dan Fuhry - 2008 + * + * This script is in the public domain. Use it for good, not evil. + */ + +var ajax; +var is_playing = false, current_track = -1, current_track_length, current_track_pos, ct_advance_timeout = false, ct_counter = false, playlist_md5 = false; + +function ajaxGet(uri, f) +{ + if (window.XMLHttpRequest) + { + ajax = new XMLHttpRequest(); + } + else + { + if (window.ActiveXObject) { + ajax = new ActiveXObject("Microsoft.XMLHTTP"); + } + else + { + alert('AmaroK client-side runtime error: No AJAX support, unable to continue'); + return; + } + } + ajax.onreadystatechange = f; + ajax.open('GET', uri, true); + ajax.setRequestHeader( "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT" ); + ajax.send(null); +} + +function ajaxPost(uri, parms, f) +{ + if (window.XMLHttpRequest) + { + ajax = new XMLHttpRequest(); + } + else + { + if (window.ActiveXObject) + { + ajax = new ActiveXObject("Microsoft.XMLHTTP"); + } + else + { + alert('AmaroK client-side runtime error: No AJAX support, unable to continue'); + return; + } + } + ajax.onreadystatechange = f; + ajax.open('POST', uri, true); + ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + // Setting Content-length in Safari triggers a warning + if ( !is_Safari ) + { + ajax.setRequestHeader("Content-length", parms.length); + } + ajax.setRequestHeader("Connection", "close"); + ajax.send(parms); +} + +function setAjaxLoading() +{ + $('ajax_status').object.src = img_ajax; +} + +function unsetAjaxLoading() +{ + $('ajax_status').object.src = 'about:blank'; +} + +var refresh_playlist = function() +{ + setAjaxLoading(); + ajaxGet('/action.json/refresh', function() + { + if ( ajax.readyState == 4 && ajax.status == 200 ) + { + unsetAjaxLoading(); + var response = (' ' + ajax.responseText).substr(1); + // quickie JSON parser :) + response = eval('(' + response + ')'); + // has the playlist been modified? + if ( playlist_md5 ) + { + if ( response.playlist_hash != playlist_md5 ) + { + // playlist has changed, reload + window.location.reload(); + return false; + } + } + playlist_md5 = response.playlist_hash; + // update track number + if ( response.current_track != current_track ) + { + var ot_id = 'track_' + current_track; + var nt_id = 'track_' + response.current_track; + current_track = response.current_track; + if ( $(ot_id).hasClass('current') ) + { + $(ot_id).rmClass('current'); + } + if ( ! $(nt_id).hasClass('current') ) + { + $(nt_id).addClass('current'); + } + } + // update playing status + var img = $('btn_playpause').object.getElementsByTagName('img')[0]; + is_playing = response.is_playing; + if ( is_playing ) + { + img.src = img_pause; + } + else + { + img.src = img_play; + } + // update volume + if ( response.volume != current_volume ) + { + set_volume_fill(response.volume); + current_volume = response.volume; + } + // auto-refresh on track advance + if ( ct_advance_timeout ) + { + clearTimeout(ct_advance_timeout); + } + var time_remaining = response.current_track_length - response.current_track_pos; + current_track_length = response.current_track_length; + current_track_pos = response.current_track_pos; + if ( ct_counter ) + clearInterval(ct_counter); + update_clock(); + if ( is_playing ) + { + ct_advance_timeout = setTimeout(refresh_playlist, ( 1000 * time_remaining )); + ct_counter = setInterval(update_clock, 1000); + } + } + }); +} + +function player_action(action) +{ + var act2 = action; + setAjaxLoading(); + ajaxGet('/action.json/' + action, function() + { + if ( ajax.readyState == 4 && ajax.status == 200 ) + { + unsetAjaxLoading(); + refresh_playlist(); + } + }); +} + +function jump_to_song(tid) +{ + setAjaxLoading(); + if ( tid == current_track ) + return false; + ajaxGet('/action.json/jump/' + tid, function() + { + if ( ajax.readyState == 4 && ajax.status == 200 ) + { + unsetAjaxLoading(); + var response = (' ' + ajax.responseText).substr(1); + // quickie JSON parser :) + response = eval('(' + response + ')'); + + // update track number + var ot_id = 'track_' + current_track; + var nt_id = 'track_' + tid; + current_track = tid; + if ( $(ot_id).hasClass('current') ) + { + $(ot_id).rmClass('current'); + } + if ( ! $(nt_id).hasClass('current') ) + { + $(nt_id).addClass('current'); + } + // update playing status + var img = $('btn_playpause').object.getElementsByTagName('img')[0]; + is_playing = true; + img.src = img_play; + // auto-refresh on track advance + if ( ct_advance_timeout ) + { + clearTimeout(ct_advance_timeout); + } + if ( ct_counter ) + clearInterval(ct_counter); + var time_remaining = response.current_track_length - response.current_track_pos; + current_track_length = response.current_track_length; + current_track_pos = response.current_track_pos; + if ( is_playing ) + { + ct_advance_timeout = setTimeout(refresh_playlist, ( 1000 * time_remaining )); + update_clock(); + ct_counter = setInterval(update_clock, 1000); + } + } + }); +} + +function update_clock() +{ + var str = secs_to_string(current_track_pos) + '/' + secs_to_string(current_track_length); + $('playmeter').object.innerHTML = str; + current_track_pos++; +} + +function secs_to_string(time) +{ + var count_seconds = time % 60; + var count_minutes = ( time - count_seconds ) / 60; + return fill_zeroes(count_minutes) + ':' + fill_zeroes(count_seconds); +} + +function fill_zeroes(str, len) +{ + if ( !len ) + len = 2; + if ( typeof(str) == 'number' && str == 0 ) + str = '0'; + str = String(str); + while ( str.length < len ) + { + str = '0' + str; + } + return str; +} + +window.onload = refresh_playlist; +setInterval(refresh_playlist, 10000); + diff -r cddc2ba706d6 -r 860ba7141641 scripts/domutils.js --- a/scripts/domutils.js Sun Mar 23 15:24:06 2008 -0400 +++ b/scripts/domutils.js Sun Mar 23 20:24:33 2008 -0400 @@ -11,23 +11,11 @@ this.object = ( typeof(id) == 'object' ) ? id : document.getElementById(id); if ( !this.object ) { - console.warn('Dynano: requested object is bad. id parameter follows.'); - console.debug(id); - console.debug(tinyMCE.getInstanceById(id)); this.object = false; return this; } this.height = __DNObjGetHeight(this.object); this.width = __DNObjGetWidth(this.object); - - if ( this.object.tagName == 'TEXTAREA' && typeof(tinyMCE) == 'object' ) - { - this.object.dnIsMCE = 'no'; - this.switchToMCE = DN_switchToMCE; - this.destroyMCE = DN_destroyMCE; - this.getContent = DN_mceFetchContent; - this.setContent = DN_mceSetContent; - } } function __DNObjGetHeight(o) { return o.offsetHeight; @@ -91,114 +79,6 @@ return left_offset; } -function DN_switchToMCE(performWikiTransform) -{ - if ( !this.object.id ) - this.object.id = 'textarea_' + Math.floor(Math.random() * 1000000); - if ( !this.object.name ) - this.object.name = 'textarea_' + Math.floor(Math.random() * 1000000); - // Updated for TinyMCE 3.x - if ( performWikiTransform ) - { - this.object.value = DN_WikitextToXHTML(this.object.value); - } - // If tinyMCE init hasn't been called yet, do it now. - if ( !tinymce_initted ) - { - enano_tinymce_options.mode = 'exact'; - enano_tinymce_options.elements = this.object.id; - initTinyMCE(); - this.object.dnIsMCE = 'yes'; - return true; - } - else - { - tinyMCE.execCommand("mceAddControl", true, this.object.id); - this.object.dnIsMCE = 'yes'; - } - return this; -} - -function DN_destroyMCE(performWikiTransform) -{ - //if ( !this.object.dn_is_mce ) - // return this; - if ( this.object.id ) - { - // TinyMCE 2.x - // tinyMCE.removeMCEControl(this.object.name); - // TinyMCE 3.x - var ed = tinyMCE.getInstanceById(this.object.id); - if ( ed ) - { - if ( !tinyMCE.execCommand("mceRemoveEditor", false, this.object.id) ) - alert('could not destroy editor'); - if ( performWikiTransform ) - { - this.object.value = DN_XHTMLToWikitext(this.object.value); - } - } - } - this.object.dnIsMCE = 'no'; - return this; -} - -function DN_mceFetchContent() -{ - if ( this.object.name ) - { - var text = this.object.value; - if ( tinyMCE.get(this.object.id) ) - { - var editor = tinyMCE.get(this.object.id); - text = editor.getContent(); - } - return text; - } - else - { - return this.object.value; - } -} - -function DN_mceSetContent(text) -{ - if ( this.object.name ) - { - this.object.value = text; - if ( tinyMCE.get(this.object.id) ) - { - var editor = tinyMCE.get(this.object.id); - editor.setContent(text); - } - } - else - { - this.object.value = text; - } -} - -// A basic Wikitext to XHTML converter -function DN_WikitextToXHTML(text) -{ - text = text.replace(/^===[\s]*(.+?)[\s]*===$/g, '

$1

'); - text = text.replace(/'''(.+?)'''/g, '$1'); - text = text.replace(/''(.+?)''/g, '$1'); - text = text.replace(/\[(http|ftp|irc|mailto):([^ \]])+ ([^\]]+?)\]/g, '$4'); - return text; -} - -// Inverse of the previous function -function DN_XHTMLToWikitext(text) -{ - text = text.replace(/

(.+?)<\/h3>/g, '=== $1 ==='); - text = text.replace(/<(b|strong)>(.+?)<\/(b|strong)>/g, "'''$2'''"); - text = text.replace(/<(i|em)>(.+?)<\/(i|em)>/g, "''$2''"); - text = text.replace(/(.+?)<\/a>/g, '[$1 $2]'); - text = text.replace(/<\/?p>/g, ''); - return text; -} - DNobj.prototype.addClass = function(clsname) { addClass(this.object, clsname); return this; }; DNobj.prototype.rmClass = function(clsname) { rmClass( this.object, clsname); return this; }; DNobj.prototype.hasClass = function(clsname) { return hasClass(this.object, clsname); }; @@ -207,3 +87,11 @@ DNobj.prototype.Left = function() { /* return this.object.offsetLeft; */ return __DNObjGetLeft(this.object); } DNobj.prototype.Top = function() { /* return this.object.offsetTop; */ return __DNObjGetTop( this.object); } +// Equivalent to PHP trim() function +function trim(text) +{ + text = text.replace(/^([\s]+)/, ''); + text = text.replace(/([\s]+)$/, ''); + return text; +} + diff -r cddc2ba706d6 -r 860ba7141641 scripts/volume.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/volume.js Sun Mar 23 20:24:33 2008 -0400 @@ -0,0 +1,65 @@ +/** + * Volume widget presentation code + * + * Web control interface script for Amarok + * Written by Dan Fuhry - 2008 + * + * This script is in the public domain. Use it for good, not evil. + */ + +var current_volume = 0; + +function set_volume_fill(amount) +{ + amount = 10 * ( Math.round(amount / 10) ); + if ( amount == 0 ) + amount = -10; + for ( var i = 0; i <= amount; i += 10 ) + { + if ( !$('volbtn_' + i).hasClass('volume_button_active') ) + { + $('volbtn_' + i).addClass('volume_button_active'); + } + } + for ( ; i <= 100; i += 10 ) + { + if ( $('volbtn_' + i).hasClass('volume_button_active') ) + { + $('volbtn_' + i).rmClass('volume_button_active'); + } + } +} + +function volume_over(amount) +{ + set_volume_fill(amount); +} + +function volume_out() +{ + set_volume_fill(current_volume); +} + +function set_volume(level) +{ + setAjaxLoading(); + if ( level == current_volume ) + return false; + ajaxGet('/action.json/volume/' + level, function() + { + if ( ajax.readyState == 4 && ajax.status == 200 ) + { + unsetAjaxLoading(); + var response = (' ' + ajax.responseText).substr(1); + // quickie JSON parser :) + response = eval('(' + response + ')'); + // update volume + if ( response.volume != current_volume ) + { + set_volume_fill(response.volume); + current_volume = response.volume; + } + } + }); +} + diff -r cddc2ba706d6 -r 860ba7141641 themes/funkymonkey/images/ajax.gif Binary file themes/funkymonkey/images/ajax.gif has changed diff -r cddc2ba706d6 -r 860ba7141641 themes/funkymonkey/images/amarok.gif Binary file themes/funkymonkey/images/amarok.gif has changed diff -r cddc2ba706d6 -r 860ba7141641 themes/funkymonkey/images/volume.png Binary file themes/funkymonkey/images/volume.png has changed diff -r cddc2ba706d6 -r 860ba7141641 themes/funkymonkey/playlist.tpl --- a/themes/funkymonkey/playlist.tpl Sun Mar 23 15:24:06 2008 -0400 +++ b/themes/funkymonkey/playlist.tpl Sun Mar 23 20:24:33 2008 -0400 @@ -12,6 +12,12 @@ AmaroK playlist + {foreach from=$scripts item=script} {/foreach} @@ -19,11 +25,29 @@
- « PrevTrk - Play - Pause - Stop - NextTrk » +  + AmaroK web control + « PrevTrk + Play + Stop + NextTrk » +   + --:--/--:-- +   + Volume: +             +
@@ -37,12 +61,12 @@ {foreach key=tid item=track from=$playlist} {strip} - + {$tid+1} - {if $active != $tid}{/if} + {$track.title|escape} - {if $active != $tid}{/if} + {$track.artist|escape} {$track.album|escape} diff -r cddc2ba706d6 -r 860ba7141641 themes/funkymonkey/style.css --- a/themes/funkymonkey/style.css Sun Mar 23 15:24:06 2008 -0400 +++ b/themes/funkymonkey/style.css Sun Mar 23 20:24:33 2008 -0400 @@ -57,6 +57,7 @@ div.playbar-inner { padding: 5px; + line-height: 22px; } a img { @@ -72,3 +73,24 @@ color: white; } +div#playlist tr.current a.tracklink { + color: #ffff00; +} + +span#playmeter, span#volume_wrap { + position: relative; + top: -7px; +} + +a.volume_button { + padding-right: 10px; + margin-right: 1px; + background-color: #33395d; + border: 1px solid #909090; +} + +a.volume_button_active { + background-color: #9090c2; + border-color: #d0d0d0; +} + diff -r cddc2ba706d6 -r 860ba7141641 webcontrol.php --- a/webcontrol.php Sun Mar 23 15:24:06 2008 -0400 +++ b/webcontrol.php Sun Mar 23 20:24:33 2008 -0400 @@ -25,6 +25,7 @@ define('SMARTY_DIR', './smarty/'); require('smarty/Smarty.class.php'); require('playlist.php'); +require('json.php'); require('ajax.php'); status('initializing Smarty'); @@ -63,6 +64,7 @@ // setup handlers status('initializing handlers'); $httpd->add_handler('index', 'function', 'amarok_playlist'); + $httpd->add_handler('action.json', 'function', 'ajax_request_handler'); $httpd->add_handler('scripts', 'dir', './scripts'); $httpd->add_handler("themes/$theme", 'dir', "./themes/$theme"); $httpd->allow_dir_list = true;