functions.php
author Dan
Fri, 12 Jun 2009 13:49:22 -0400
changeset 77 e5f1f45ea7e2
parent 75 2f39cb7f54c4
permissions -rw-r--r--
Added ability to configure from iPhone; changed up iPhone interface a bit

<?php

/**
 * Utility functions
 * 
 * Greyhound - real web management for Amarok
 * Copyright (C) 2008 Dan Fuhry
 *
 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
 *
 * 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 details.
 */

/**
 * Utility/reporting functions
 */
 
/**
 * Report a fatal error and exit
 * @param string Error message
 */

function burnout($msg)
{
  global $use_colors;
  $h = @fopen('php://stderr', 'w');
  if ( $use_colors )
  {
    fwrite($h, "\x1B[31;1m[Greyhound] fatal: \x1B[37;1m");
    fwrite($h, "$msg\x1B[0m\n");
  }
  else
  {
    fwrite($h, "[Greyhound] fatal: $msg\n");
  }
  fclose($h);
  exit(1);
}

/**
 * Print a stylized status message, compatible with Linux consoles. Falls back to no control characters if on unsupported terminal.
 * @param string Status message
 */

function status($msg)
{
  global $use_colors;
  $h = @fopen('php://stderr', 'w');
  $label = ( defined('HTTPD_WS_CHILD') ) ? 'Child ' . substr(strval(getmypid()), -3) : 'Greyhound';
  if ( $use_colors )
  {
    fwrite($h, "\x1B[32;1m[$label] \x1B[32;0m$msg\x1B[0m\n");
  }
  else
  {
    fwrite($h, "[$label] $msg\n");
  }
  fclose($h);
}

/**
 * Print a stylized warning message, compatible with Linux consoles
 * @param string message message
 */

function warning($msg)
{
  global $use_colors;
  $h = @fopen('php://stderr', 'w');
  if ( $use_colors )
  {
    fwrite($h, "\x1B[33;1m[Greyhound] \x1B[0m\x1B[33mWarning:\x1B[0m $msg\n");
  }
  else
  {
    fwrite($h, "[Greyhound] Warning: $msg\n");
  }
  fclose($h);
}

/**
 * Performs an action with DCOP.
 * @param string DCOP component, e.g. player, playlist, playlistbrowser, ...
 * @param string Action to perform, e.g. stop, play, ...
 * @param string additional parameters... (NOT IMPLEMENTED)
 * @return mixed output of DCOP command
 */

function dcop_action($component, $action)
{
  file_put_contents('php://stderr', "[dcop] start...");
  if ( function_exists('proc_open') )
  {
    $descriptorspec = array(
        0 => array('pipe', 'r'),
        1 => array('pipe', 'w'),
        2 => array('pipe', 'w')
      );
    $dcop_command = "dcop amarok $component $action";
    $dcop_process = proc_open($dcop_command, $descriptorspec, $dcop_pipes);
    do
    {
      $status = proc_get_status($dcop_process);
      usleep(2000);
    } while ( $status['running'] );
    pcntl_waitpid($status['pid'], $status);
    $output = fgets($dcop_pipes[1], 1024);
    proc_close($dcop_process);
  }
  else
  {
    $tmpfile = tempnam('amaweb', '');
    if ( !$tmpfile )
      burnout('tempnam() failed us');
    
    // This is freezing up for some reason.
    system("dcop amarok $component $action > $tmpfile");
    $output = @file_get_contents($tmpfile);
    @unlink($tmpfile);
  }
  file_put_contents('php://stderr', "got it...\r");
  $output = trim($output);
  
  // detect type of output
  if ( $output == 'true' )
    return true;
  else if ( $output == 'false' )
    return false;
  else if ( preg_match('/^-?[0-9]+/', $output) )
    return intval($output);
  else
    return $output;
}

/**
 * Rebuilds the copy of the playlist in RAM
 */

$playlist_last_md5 = '';

function rebuild_playlist($force = false, $from_ipc = false)
{
  // import what we need
  global $playlist, $amarok_home;
  // sync and load the playlist file
  // if we're coming from IPC, don't call DCOP, just load the existing file
  // this prevents the file from being generated 20 times and thus sometimes
  // causing simplexml to read a half-written file
  if ( !$from_ipc )
  {
    $playlist_file = dcop_action('playlist', 'saveCurrentPlaylist');
  }
  else
  {
    $playlist_file = $from_ipc;
  }
  // do we have amarok's home?
  if ( !$amarok_home )
    $amarok_home = dirname($playlist_file);
  // check MD5 - if it's not changed, exit to save CPU cycles
  global $playlist_last_md5;
  $effective_md5 = md5_playlist_file($playlist_file);
  if ( $playlist_last_md5 == $effective_md5 && !$force )
  {
    return true;
  }
  status('Rebuilding playlist cache');
  $playlist_last_md5 = $effective_md5;
  $originalplaylist = $playlist_file;
  $xml = false;
  $unlinkplaylist = false;
  while ( !$xml )
  {
    // start XML parser
    try
    {
      $xml = @simplexml_load_file($playlist_file);
      if ( $xml )
        break;
      
      // if we can't load it, just wait - another process might have it loaded
      warning('Could not load playlist file; maybe another Greyhound thread has it loaded? Waiting half a second.');
      usleep(500000);
    }
    catch ( Exception $e )
    {
      burnout("Caught exception trying to load playlist file:\n$e");
    }
  }
  
  $attribs = $xml->attributes();
  if ( @$attribs['product'] != 'Amarok' )
  {
    burnout('Playlist is not in Amarok format');
  }
  $playlist = array();
  foreach ( $xml->children() as $child )
  {
    $attribs = $child->attributes();
    $item = array(
        'uri'    => $attribs['uri'],
        'title'  => strval($child->Title),
        'artist' => strval($child->Artist),
        'album'  => strval($child->Album),
        'length' => seconds_to_str(intval($child->Length)),
        'length_int' => intval($child->Length)
      );
    $playlist[] = $item;
  }
  if ( $unlinkplaylist )
  {
    unlink($playlist_file);
  }
  // tell all other worker threads to reload the playlist as well
  global $httpd;
  if ( is_object($httpd) && !$from_ipc )
  {
    $httpd->threader->ipc_send(array(
        'action' => 'reloadplaylist',
        'propagate' => true,
        'playlist_file' => $playlist_file
      ));
  }
}

function rebuild_playlist_ipc($command, $threader)
{
  status('IPC playlist rebuild received');
  if ( isset($command['playlist']) )
  {
    status('IPC playlist rebuild: using playlist sent in IPC packet; length: ' . strlen($command['playlist']));
    $GLOBALS['playlist'] = $command['playlist'];
  }
  else
  {
    rebuild_playlist(true, $command['playlist_file']);
  }
}

/**
 * Builds the correct MD5 check for the specified playlist XML file. This is designed to base on the list of actual tracks, disregarding
 * the rest of the text in the XML file.
 * @param string Path to playlist
 * @return string hash
 */

function md5_playlist_file($file)
{
  $contents = @file_get_contents($file);
  if ( empty($contents) )
    return false;
  $count = preg_match_all('/uniqueid="([a-fA-F0-9]+?)"/', $contents, $matches);
  $matches = implode("", $matches[1]);
  if ( empty($matches) )
  {
    // sometimes current.xml has blank unique IDs
    $count = preg_match_all('/url="([^"]+?)"/', $contents, $matches);
    $matches = implode("", $matches[1]);
  }
  return md5($matches);
}

/**
 * Converts a number to minute:second format
 * @param int Seconds
 * @return string format: mm:ss
 */

function seconds_to_str($secs)
{
  $seconds = $secs % 60;
  $minutes = ( $secs - $seconds ) / 60;
  $seconds = strval($seconds);
  $minutes = strval($minutes);
  if ( strlen($seconds) < 2 )
    $seconds = "0$seconds";
  if ( strlen($minutes) < 2 )
    $minutes = "0$minutes";
  return "$minutes:$seconds";
}

/**
 * Loads the specified theme into Smarty
 * @param string Theme ID
 * @return object Smarty object
 */

function load_theme($theme_id)
{
  global $httpd;
  static $smarty = array();
  if ( $theme_id === '__free__' )
  {
    $smarty = array();
    return false;
  }
  if ( !isset($smarty[$theme_id]) )
  {
    $smarty[$theme_id] = new Smarty();
    $smarty[$theme_id]->template_dir = GREY_ROOT . "/themes/$theme_id";
    if ( !is_dir("./compiled/$theme_id") )
      @mkdir("./compiled/$theme_id");
    $smarty[$theme_id]->compile_dir  = "./compiled/$theme_id";
    $smarty[$theme_id]->config_dir = "./config";
    $httpd->add_handler("themes/$theme_id", 'dir', GREY_ROOT . "/themes/$theme_id");
  }
  return $smarty[$theme_id];
}

/**
 * Implementation of the "which" command in native PHP.
 * @param string command
 * @return string path to executable, or false on failure
 */

function which($executable)
{
  $path = ( isset($_ENV['PATH']) ) ? $_ENV['PATH'] : ( isset($_SERVER['PATH']) ? $_SERVER['PATH'] : false );
  if ( !$path )
    // couldn't get OS's PATH
    return false;
    
  $win32 = ( PHP_OS == 'WINNT' || PHP_OS == 'WIN32' );
  $extensions = $win32 ? array('.exe', '.com', '.bat') : array('');
  $separator = $win32 ? ';' : ':';
  $paths = explode($separator, $path);
  foreach ( $paths as $dir )
  {
    foreach ( $extensions as $ext )
    {
      $fullpath = "$dir/{$executable}{$ext}";
      if ( file_exists($fullpath) && is_executable($fullpath) )
      {
        return $fullpath;
      }
    }
  }
  return false;
}

/**
 * Reload the config.
 */

function grey_reload_config()
{
  global $httpd;
  status('reloading the config');
  
  if ( file_exists('./greyhound-config.php') )
  {
    require('./greyhound-config.php');
  }
  else
  {
    // ignore this, it allows using a different config file when a Mercurial repository
    // exists in Greyhound's root directory (to allow the devs to have their own config
    // separate from the default)
    
    if ( @is_dir(GREY_ROOT . '/.hg') )
    {
      require(GREY_ROOT . '/config.dev.php');
    }
    else
    {
      require(GREY_ROOT . '/config.php');
    }
  }
  
  foreach ( array('public', 'enable_ipv4', 'enable_ipv6', 'allowcontrol', 'theme', 'allow_fork', 'use_auth', 'auth_data', 'configpass') as $var )
  {
    if ( isset($$var) )
    {
      $GLOBALS[$var] = $$var;
    }
  }
}

/**
 * Decodes a hex string.
 * @param string $hex The hex code to decode
 * @return string
 */

function hexdecode($hex)
{
  $hex = str_split($hex, 2);
  $bin_key = '';
  foreach($hex as $nibble)
  {
    $byte = chr(hexdec($nibble));
    $bin_key .= $byte;
  }
  return $bin_key;
}