Added artwork spriting support. Artwork is now displayed using a gigantic CSS sprite instead of hundreds of little images. GD required.
<?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, $auth_data; if ( $use_auth ) { if ( !isset($_SERVER['PHP_AUTH_USER']) ) { $httpd->header('WWW-Authenticate: basic'); $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); return true; } if ( !isset($auth_data[$_SERVER['PHP_AUTH_USER']]) ) { $httpd->header('WWW-Authenticate: basic'); $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); return true; } else if ( $auth_data[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW'] ) { $httpd->header('WWW-Authenticate: basic'); $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); return true; } } $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->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']);}