#!/usr/local/bin/php
<?php
/*
MusicSync v1.0.0
Copyright (c) 2005 by Ryan Grove <ryan@wonko.com>. All rights reserved.
 
Synchronizes music files in a destination directory with those in a source
directory. Ideal for keeping a portable music player in sync with a master
music collection.
 
See http://wiki.wonko.com/software/musicsync/ for requirements, documentation,
and usage information.
 
License:
 
	This program is free software; you can redistribute it 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 more details.
 
	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the Free Software Foundation, Inc.,
	59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
 
// -- Default Options ---------------------------------------------------------
 
$options = array();
 
/*
Source directory (the directory containing the most up-to-date files). If set,
unless overridden by the commandline option, this value will be used.
*/
$options['source'] = null;
 
/*
Destination directory (the directory containing files that are likely to be out
of sync). If set, unless overridden by the commandline option, this value will
be used.
*/
$options['destination'] = null;
 
/*
File extensions to treat as music files. Separate multiple extensions with
commas (i.e., 'aac,mp3,ogg').
*/
$options['extensions'] = 'aac,mp3,ogg,wav,wma';
 
/*
Name of a file containing a list of files and directories that should be
ignored. One filename or directory should be specified per line. Wildcards
(such as *) are supported.
*/
$options['ignore'] = null;
 
/*
Whether or not to run in quiet mode. Will be overridden by the commandline
option if it is used.
*/
$options['quiet'] = false;
 
// -- Constants ---------------------------------------------------------------
 
define('VERSION', '1.0.0');
 
// -- Main Application --------------------------------------------------------
 
set_time_limit(0);
 
getOptions();
 
$options['extensions'] = explode(',', strtolower($options['extensions']));
 
// Check that the source directory exists.
if (!is_dir($options['source']))
	error("Source directory does not exist: {$options['source']}");
 
// Parse ignore list.
if (!is_null($options['ignore']))
{
	if (!is_file($options['ignore']))
		error("Ignore file does not exist: {$options['ignore']}");
 
	if (false === ($options['ignoreList'] = file($options['ignore'])))
		error("Error loading ignore file: {$options['ignore']}");
 
	// Convert wildcards to regular expressions.
	$search  = array('\?', '\*');
	$replace = array('.', '.*');
 
	foreach($options['ignoreList'] as $key => $ignore)
	{
		if (!strlen(trim($ignore)))
		{
			unset($options['ignoreList'][$key]);
			continue;
		}
 
		$options['ignoreList'][$key] = '/^'.str_replace($search, $replace,
			preg_quote(trim($ignore), '/')).'$/';
	}
}
 
// Begin synchronizing.
sync($options['source'], $options['destination']);
 
// -- Functions ---------------------------------------------------------------
 
function copyFile($source, $dest)
{
	if (!@copy($source, $dest))
		error("Unable to copy file \"$source\" to \"$dest\".");
 
	return true;
}
 
function displayUsage()
{
	echo "MusicSync v".VERSION."\n";
	echo "Copyright (c) 2005 by Ryan Grove <ryan@wonko.com>. All rights reserved.\n\n";
 
	echo "Usage:\n\n";
 
	echo "	musicsync.php [-q] [-e ext1,ext2 ...] [-i file] <sourcedir> <destdir>\n\n";
 
	echo "	<sourcedir>\n";
	echo "			Source directory (the directory containing the most\n";
	echo "			up-to-date files). If the path contains spaces, enclose it in\n";
	echo "			quotes (i.e., \"C:\\My Music\").\n\n";
 
	echo "	<destdir>\n";
	echo "			Destination directory (the directory containing files that\n";
	echo "			are likely to be out of sync). If the path contains spaces, enclose\n";
	echo "			it in quotes (i.e., \"D:\\Portable Music\").\n\n";
 
	echo "	-e <extensions> --extensions <extensions>\n";
	echo "			Comma-separated list of file extensions to be synchronized. If not\n";
	echo "			specified, a default list of common music file extensions will be\n";
	echo "			used.\n\n";
 
	echo "	-i <filename> --ignore <filename>\n";
	echo "			Name of a file containing a list of files and directories that\n";
	echo "			should be ignored. One filename or directory should be specified\n";
	echo "			per line. Wildcards (such as *) are supported.\n\n";
 
	echo "	-q --quiet\n";
	echo "			Run in quiet mode. Only errors are reported.\n\n";
 
	echo "Example:\n\n";
 
	echo "	musicsync.php -e mp3,ogg,wav c:\\music d:\\\n\n";
	exit;
}
 
function error($message)
{
	echo "\n$message";
	exit(1);
}
 
function getDirectory($directory)
{
	global $options;
 
	$result = array('dirs' => array(), 'files' => array());
 
	if (false === ($handle = opendir($directory)))
		return false;
 
	while (false !== ($file = readdir($handle)))
	{
		if ($file == '.' || $file == '..')
			continue;
 
		$fullPath = "$directory/$file";
 
		if (is_dir($fullPath))
		{
			// Check that the directory isn't in the ignore list.
			if (ignored($fullPath))
				continue;
 
			$result['dirs'][] = $file;
		}
		else
		{
			$extension = substr($file, strrpos($file, '.') + 1);
 
			if (!$extension)
				continue;
 
			// Check that the extension is in the extension list.
			if (!in_array(strtolower($extension), $options['extensions']))
				continue;
 
			// Check that the file isn't in the ignore list.
			if (ignored($fullPath))
				continue;
 
			$result['files'][] = $file;
		}
	}
 
	closedir($handle);
 
	sort($result['dirs']);
	sort($result['files']);
 
	return $result;
}
 
function getOptions()
{
	global $options;
 
	$haveSource      = false;
	$haveDestination = false;
 
	for ($i = 1; $i < $_SERVER['argc']; $i++)
	{
		switch($_SERVER['argv'][$i])
		{
			case '-e':
			case '--extensions':
				$options['extensions'] = $_SERVER['argv'][++$i];
				break;
 
			case '-i':
			case '--ignore':
				$options['ignore'] = $_SERVER['argv'][++$i];
				break;
 
			case '-q':
			case '--quiet':
				$options['quiet'] = true;
				break;
 
			case '-?':
			case '-h':
			case '--help':
				displayUsage();
				break;
 
			default:
				if (!$haveSource)
				{
					$options['source'] = $_SERVER['argv'][$i];
					$haveSource        = true;
				}
				elseif (!$haveDestination)
				{
					$options['destination'] = $_SERVER['argv'][$i];
					$haveDestination        = true;
				}
				else
					displayUsage();
		}
	}
}
 
function ignored($file)
{
	global $options;
 
	// Look for the file in the ignore list (if any).
	if (isset($options['ignoreList']))
	{
		foreach($options['ignoreList'] as $ignore)
		{
			if (preg_match($ignore, $file))
				return true;
		}
	}
 
	return false;
}
 
function rmdirr($dirname)
{
	if (false === ($files = getDirectory($dirname)))
		error("Unable to remove directory: $dirname");
 
	// Delete files.
	foreach($files['files'] as $file)
	{
		if (!@unlink($file))
			error("Unable to delete file: $dirname/$file");
	}
 
	// Recursively delete subdirectories.
	foreach($files['dirs'] as $dir)
		rmdirr("$dirname/$dir");
 
	return true;
}
 
function sync($source, $destination)
{
	global $options;
 
	// Make sure the source and destination aren't in the ignore list.
	if (ignored($source) || ignored($destination))
		return false;
 
	// Get array of files and directories in source.
	if (false === ($sourceList = getDirectory($source)))
		error("Invalid source directory: $source");
 
	// Attempt to create destination if it doesn't exist.
	if (!is_dir($destination))
	{
		echo !$options['quiet'] ? "Creating directory $destination\n" : '';
 
		if (!@mkdir($destination, 0775))
			error("Unable to create directory: $destination");
	}
 
	// Get array of files and directories in destination.
	if (false === ($destList = getDirectory($destination)))
		error("Invalid destination directory: $destination");
 
	// Remove orphaned subdirectories.
	$orphanedDirs = array_diff($destList['dirs'], $sourceList['dirs']);
 
	foreach($orphanedDirs as $key => $dir)
	{
		if (ignored("$destination/$dir"))
			continue;
 
		unset($destList['dirs'][$key]);
		rmdirr("$destination/$dir");
	}
 
	// Remove orphaned files.
	$orphanedFiles = array_diff($destList['files'], $sourceList['files']);
 
	foreach($orphanedFiles as $key => $file)
	{
		if (ignored("$destination/$file"))
			continue;
 
		unset($destList['files'][$key]);
 
		if (!@unlink("$destination/$file"))
			error("Unable to remove file: $destination/$file");
	}
 
	// Synchronize existing files.
	foreach($destList['files'] as $file)
	{
		if (ignored("$destination/$file"))
			continue;
 
		if (filesize("$source/$file") != filesize("$destination/$file"))
		{
			echo !$options['quiet'] ? "Updating $destination/$file\n" : '';
			copyFile("$source/$file", "$destination/$file");
		}
	}
 
	// Create missing files.
	$missingFiles = array_diff($sourceList['files'], $destList['files']);
 
	foreach($missingFiles as $file)
	{
		if (ignored("$destination/$file"))
			continue;
 
		echo !$options['quiet'] ? "Adding $destination/$file\n" : '';
		copyFile("$source/$file", "$destination/$file");
	}
 
	// Recursively synchronize subdirectories.
	foreach($sourceList['dirs'] as $dir)
		sync("$source/$dir", "$destination/$dir");
}
?>