playlist.php
author Dan
Tue, 26 May 2009 15:25:30 -0400
changeset 69 73780a159e15
parent 44 92dd253f501c
permissions -rw-r--r--
DCOP: added safeguards for mysterious zombie behavior

<?php

/**
 * Playlist displayer
 *
 * 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.
 */

function amarok_playlist($httpd, $socket)
{
  global $theme, $playlist, $allowcontrol;
  global $use_auth;
  
  if ( !session_check() )
  {
    $httpd->header('HTTP/1.1 307 Temporary Redirect');
    $httpd->header('Location: /login');
    
    return;
  }
  
  $iphone = ( ( strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') ||
       strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') ||
       strpos($_SERVER['HTTP_USER_AGENT'], 'BlackBerry') ||
       isset($_GET['m']) )
       && !isset($_GET['f'])
       );
  $theme_id = ( $iphone ) ? 'iphone' : $theme;
  $smarty = load_theme($theme_id);
  
  $active = dcop_action('playlist', 'getActiveIndex');
  $smarty->assign('theme', $theme_id);
  $smarty->assign('playlist', $playlist);
  $smarty->assign('active', $active);
  $smarty->assign('scripts', array(
      'ajax.js',
      'domutils.js',
      'volume.js',
      'dom-drag.js',
      'position.js'
    ));
  $smarty->assign('allow_control', $allowcontrol);
  $smarty->assign('use_auth', $use_auth);
  $smarty->assign('greyhound_version', GREY_VERSION);
  $smarty->register_function('sprite', 'smarty_function_sprite');
  $smarty->display('playlist.tpl');
}

function artwork_request_handler($httpd, $socket)
{
  global $amarok_home;
  
  // get PATH_INFO
  $pathinfo = @substr(@substr($_SERVER['REQUEST_URI'], 1), @strpos(@substr($_SERVER['REQUEST_URI'], 1), '/')+1);
  
  // should we do a collage (for CSS sprites instead of sending hundreds of individual images)?
  if ( preg_match('/^collage(?:\/([0-9]+))?$/', $pathinfo, $match) )
  {
    // default size is 50px per image
    $collage_size = ( isset($match[1]) ) ? intval($match[1]) : 50;
    
    $artwork_dir = "$amarok_home/albumcovers";
    if ( !file_exists("$artwork_dir/collage_{$collage_size}.png") )
    {
      if ( !generate_artwork_collage("$artwork_dir/collage_{$collage_size}.png", $collage_size) )
      {
        echo 'Error: generate_artwork_collage() failed';
        return;
      }
    }
    
    $target_file = "$artwork_dir/collage_{$collage_size}.png";
    // we have it now, send the image through
    $fh = @fopen($target_file, 'r');
    if ( !$fh )
      return false;
    
    $httpd->header('Content-type: image/png');
    $httpd->header('Content-length: ' . filesize($target_file));
    $httpd->header('Expires: Wed, 1 Jan 2020 01:00:00 GMT');
    
    // kinda sorta a hack.
    $headers = implode("\r\n", $httpd->response_headers);
    $httpd->send_client_headers($socket, $httpd->response_code, $httpd->content_type, $headers);
    
    while ( $d = fread($fh, 10240) )
    {
      $socket->write($d);
    }
    fclose($fh);
    
    return;
  }
  
  if ( !isset($_GET['artist']) || !isset($_GET['album']) )
  {
    echo 'Please specify artist and album.';
    return;
  }
  // get hash
  $artwork_hash = md5( strtolower(trim($_GET['artist'])) . strtolower(trim($_GET['album'])) );
  $artwork_dir = "$amarok_home/albumcovers";
  if ( file_exists("$artwork_dir/large/$artwork_hash") )
  {
    // artwork file found - scale and convert to PNG
    if ( !is_dir("$artwork_dir/greyhoundthumbnails") )
    {
      if ( !@mkdir("$artwork_dir/greyhoundthumbnails") )
      {
        return false;
      }
    }
    // check for the scaled cover image
    $target_file = "$artwork_dir/greyhoundthumbnails/$artwork_hash.png";
    if ( !file_exists($target_file) )
    {
      // not scaled yet, scale to uniform 50x50 image
      $artwork_filetype = get_image_filetype("$artwork_dir/large/$artwork_hash");
      if ( !$artwork_filetype )
      {
        // image is not supported (PNG, GIF, or JPG required)
        return false;
      }
      // we'll need to copy the existing artwork file to our thumbnail dir to let scale_image() detect the type properly (it doesn't use magic bytes)
      if ( !copy("$artwork_dir/large/$artwork_hash", "$artwork_dir/greyhoundthumbnails/tmp{$artwork_hash}.$artwork_filetype") )
      {
        return false;
      }
      // finally, scale the image
      if ( !scale_image("$artwork_dir/greyhoundthumbnails/tmp{$artwork_hash}.$artwork_filetype", $target_file, 50, 50) )
      {
        return false;
      }
      // delete our temp file
      if ( !unlink("$artwork_dir/greyhoundthumbnails/tmp{$artwork_hash}.$artwork_filetype") )
      {
        echo 'Couldn\'t delete the temp file';
        return false;
      }
    }
    // we have it now, send the image through
    $fh = @fopen($target_file, 'r');
    if ( !$fh )
      return false;
    
    $httpd->header('Content-type: image/png');
    $httpd->header('Content-length: ' . filesize($target_file));
    $httpd->header('Expires: Wed, 1 Jan 2020 01:00:00 GMT');
    
    // kinda sorta a hack.
    $headers = implode("\r\n", $httpd->response_headers);
    $httpd->send_client_headers($socket, $httpd->response_code, $httpd->content_type, $headers);
    
    while ( !feof($fh) )
    {
      $socket->write(fread($fh, 51200));
    }
    fclose($fh);
  }
  else
  {
    // artwork file doesn't exist
    $ar = htmlspecialchars($_GET['artist']);
    $al = htmlspecialchars($_GET['album']);
    $httpd->send_http_error($socket, 404, "The requested artwork file for $ar:$al could not be found on this server.");
  }
}

/**
 * Generates a collage of all album art for use as a CSS sprite. Also generates a textual .map file in the format of "hash xpos ypos\n"
 * to allow retrieving positions of images. Requires GD.
 * @param string Name of the collage file. Map file will be the same filename except with the extension ".map"
 * @param int Size of each image, in pixels. Artwork images will be stretched to a 1:1 aspect ratio. Optional, defaults to 50.
 * @return bool True on success, false on failure.
 */

function generate_artwork_collage($target_file, $size = 50)
{
  // check for required GD functionality
  if ( !function_exists('imagecopyresampled') || !function_exists('imagepng') )
    return false;
  
  status("generating size $size collage");
  $stderr = fopen('php://stderr', 'w');
  if ( !$stderr )
    // this should really never fail.
    return false;
  
  // import amarok globals
  global $amarok_home;
  $artwork_dir = "$amarok_home/albumcovers";
  
  // map file path
  $mapfile = preg_replace('/\.[a-z]+$/', '', $target_file) . '.map';
  
  // open map file
  $maphandle = @fopen($mapfile, 'w');
  if ( !$maphandle )
    return false;
  
  $mapheader = <<<EOF
# this artwork collage map gives the locations of various artwork images within the collage
# format is:
# hash                           x y
# x and y are indices, not pixel values (obviously), and hash is the name of the artwork file in large/

EOF;
  fwrite($maphandle, $mapheader);
  
  // build a list of existing artwork files
  $artwork_list = array();
  if ( $dh = @opendir("$artwork_dir/large") )
  {
    while ( $fp = @readdir($dh) )
    {
      if ( preg_match('/^[a-f0-9]{32}$/', $fp) )
      {
        $artwork_list[] = $fp;
      }
    }
    closedir($dh);
  }
  else
  {
    return false;
  }
  
  // at least one image?
  if ( empty($artwork_list) )
    return false;
  
  // asort it to make sure map is predictable
  asort($artwork_list);
  
  // number of columns
  $cols = 20;
  // number of rows
  $rows = ceil( count($artwork_list) / $cols );
  
  // image dimensions
  $image_width  = $cols * $size;
  $image_height = $rows * $size;
  
  // create image
  $collage = imagecreatetruecolor($image_width, $image_height);
  
  // generator loop
  // start at row 0, column 0
  $col = -1;
  $row = 0;
  $srow = $row + 1;
  fwrite($stderr, "  -> row $srow of $rows\r");
  $time_map = microtime(true);
  foreach ( $artwork_list as $artwork_file )
  {
    // calculate where we are
    $col++;
    if ( $col == $cols )
    {
      // reached column limit, reset $cols and increment row
      $col = 0;
      $row++;
      $srow = $row + 1;
      fwrite($stderr, "  -> row $srow of $rows\r");
    }
    // x and y offset of scaled image
    $xoff = $col * $size;
    $yoff = $row * $size;
    // set offset
    fwrite($maphandle, "$artwork_file $col $row\n");
    // load image
    $createfunc = ( get_image_filetype("$artwork_dir/large/$artwork_file") == 'jpg' ) ? 'imagecreatefromjpeg' : 'imagecreatefrompng';
    $aw = @$createfunc("$artwork_dir/large/$artwork_file");
    if ( !$aw )
    {
      $aw = @imagecreatefromwbmp("$artwork_dir/large/$artwork_file");
      if ( !$aw )
      {
        // couldn't load image, silently continue
        continue;
      }
    }
    list($aw_width, $aw_height) = array(imagesx($aw), imagesy($aw));
    // scale and position image
    $result = imagecopyresampled($collage, $aw, $xoff, $yoff, 0, 0, $size, $size, $aw_width, $aw_height);
    if ( !$result )
    {
      // couldn't scale image, silently continue
      continue;
    }
    // free the temp image
    imagedestroy($aw);
  }
  $time_map = round(1000 * (microtime(true) - $time_map));
  $time_write = microtime(true);
  fclose($maphandle);
  fwrite($stderr, "  -> saving image\r");
  if ( !imagepng($collage, $target_file) )
    return false;
  imagedestroy($collage);
  $time_write = round(1000 * (microtime(true) - $time_write));
  
  $avg = round($time_map / count($artwork_list));
  
  status("collage generation complete, returning success; time (ms): map/avg/write $time_map/$avg/$time_write");
  return true;
}

/**
 * Returns an img tag showing artwork from the specified size collage sprite.
 * @param string Artist
 * @param string Album
 * @param int Collage size
 * @return string
 */

function get_artwork_sprite($artist, $album, $size = 50)
{
  // import amarok globals
  global $amarok_home;
  $artwork_dir = "$amarok_home/albumcovers";
  
  if ( !is_int($size) )
    return '';
  
  // hash of cover
  $coverid = md5(strtolower(trim($artist)) . strtolower(trim($album)));
  
  $tag = '<img alt=" " src="/spacer.gif" width="' . $size . '" height="' . $size . '" ';
  if ( file_exists("$artwork_dir/collage_{$size}.map") )
  {
    $mapdata = parse_collage_map("$artwork_dir/collage_{$size}.map");
    if ( isset($mapdata[$coverid]) )
    {
      $css_x = -1 * $size * $mapdata[$coverid][0];
      $css_y = -1 * $size * $mapdata[$coverid][1];
      $tag .= "style=\"background-image: url(/artwork/collage/$size); background-repeat: no-repeat; background-position: {$css_x}px {$css_y}px;\" ";
    }
  }
  $tag .= '/>';
  
  return $tag;
}

/**
 * Parses the specified artwork map file. Return an associative array, keys being the artwork file hashes and values being array(x, y).
 * @param string Map file
 * @return array
 */

function parse_collage_map($mapfile)
{
  if ( !file_exists($mapfile) )
    return array();
  
  $fp = @fopen($mapfile, 'r');
  if ( !$fp )
    return false;
  
  $map = array();
  while ( $line = fgets($fp) )
  {
    // parse out comments
    $line = trim(preg_replace('/#(.+)$/', '', $line));
    if ( empty($line) )
      continue;
    list($hash, $x, $y) = explode(' ', $line);
    if ( !preg_match('/^[a-f0-9]{32}$/', $hash) || !preg_match('/^[0-9]+$/', $x) || !preg_match('/^[0-9]+$/', $y) )
      // invalid line
      continue;
      
    // valid line, append map array
    $map[$hash] = array(
        intval($x),
        intval($y)
      );
  }
  fclose($fp);
  return $map;
}

/**
 * Finds out if a collage file is outdated (e.g. missing artwork images)
 * @param int Size of collage
 * @return bool true if outdated
 */

function collage_is_outdated($size = 50)
{
  global $amarok_home;
  $artwork_dir = "$amarok_home/albumcovers";
  $mapfile = "$artwork_dir/collage_{$size}.map";
  if ( !file_exists($mapfile) )
  {
    // consider it outdated if it doesn't exist
    return true;
  }
  
  // load existing image map
  $map = parse_collage_map($mapfile);
  
  // build a list of existing artwork files
  $artwork_list = array();
  if ( $dh = @opendir("$artwork_dir/large") )
  {
    while ( $fp = @readdir($dh) )
    {
      if ( preg_match('/^[a-f0-9]{32}$/', $fp) )
      {
        // found an artwork file
        if ( !isset($map[$fp]) )
        {
          // this artwork isn't in the map file, return outdated
          closedir($dh);
          status("size $size collage is outdated");
          return true;
        }
      }
    }
    closedir($dh);
  }
  
  // if we reach here, we haven't found anything missing.
  return false;
}

/**
 * Smarty function for sprite generation.
 * @access private
 */

function smarty_function_sprite($params, &$smarty)
{
  // don't perform the exhaustive check more than once per execution
  static $checks_done = array();
  
  if ( empty($params['artist']) )
    return 'Error: missing "artist" parameter';
  if ( empty($params['album']) )
    return 'Error: missing "album" parameter';
  if ( empty($params['size']) )
    $params['size'] = 50;
  
  $params['size'] = intval($params['size']);
  $size =& $params['size'];
  
  // if the collage file doesn't exist or is missing artwork, renew it
  // but only perform this check once per execution per size
  if ( !isset($checks_done[$size]) )
  {
    global $amarok_home;
    $artwork_dir = "$amarok_home/albumcovers";
    
    $collage_file = "$artwork_dir/collage_{$size}";
    $collage_is_good = file_exists("$collage_file.png") && file_exists("$collage_file.map") && !collage_is_outdated($size);
    if ( !$collage_is_good )
    {
      generate_artwork_collage("$collage_file.png", $size);
    }
    $checks_done[$size] = true;
  }
  
  return get_artwork_sprite($params['artist'], $params['album'], $params['size']);
}