A little more work on reboot support. Still not enabled, but should work eventually.
<?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']);
}