playlist.php
author Dan
Tue, 23 Sep 2008 23:26:18 -0400
changeset 50 1b4288399b1f
parent 44 92dd253f501c
permissions -rw-r--r--
Added graphical configuration, at this point only for the grey theme but others will follow soon. (This has been nearly done for two weeks or more but was on hold due to the bugs with multithreading)

<?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']);
}