#!/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"); } ?>