PHP: Random MP3 Collector

I have a pretty nifty little SanDisk MP3 player I like to use in my car. It'll play almost anything and will hold a fairly large cache of songs so that I can put it on random and pleasantly surprise myself with little nuggets of aural goodness. Unfortunately, I have to manual select files from my relatively large collection to add to the MP3 player which, in addition to being a bit of work, removes a large chunk of the serendipity from the experience. So, naturally, I wrote something to fix that.

What I wanted was something that would look at my various meticulously arranged music catalogs, search the ones I was interested in at the moment (or all of them), and grab me a random collection of tunes to drag into my MP3 player. I didn't just want a list of songs, which many music players already happily provide: I wanted the actual files, all stacked neatly together, ready for transfer. And I wanted the exact number of files I needed (or close enough), whatever their respective sizes, to fill up the space I had available.

I should first acknowledge that my preferred tool for this is probably a little non-standard. A lot of people would see an operation like this and immediately think of .Net or even PERL. But PHP can do anything PERL can do, and .NET seemed like overkill for a one click operation. And this will run on anything.

The first thing I like to do with any script I write is describe it.

/*************************************************************
randommp3.php
Purpose: Selects and copies a random collection of music files
according to the user's specification
Author:  Dan Ziemecki
**************************************************************/

Next, I like to set the key constants.

// END-USER SET PREFERENCES HERE
// File types to select from.  Surround by quotes, separated by commas
$aFILE_TYPES = array("flac","ogg","mp3");
// Top level of music folders to spider for files
// Full directory, add an array line for each, no ending slash
// Second value is an integer signifying the weight of the source
// Weight is how many times files in this source will appear in the eligible catalog
// e.g. $aSOURCE_DIR[] = array("C:\My Music\FavSongs",1);
// defaults to sourcing from script directory, a weight of "once".
$aSOURCE_DIR[] = array(end(explode('/', dirname($_SERVER['PHP_SELF']))),1);
// Directory to which the files will be written, no ending slash
// e.g. $sTARGET_DIR = "C:\My Music\Portable";
// defaults to writing to script directory\RandomMP3
# $sTARGET_DIR = end(explode('/', dirname($_SERVER['PHP_SELF'])))."\RandomMP3";
$sTARGET_DIR = "D:\Misc\RandomMp3";
// Number of MB to find and copy
$iMEGABYTES = 6144;  // 1 GB = 1024 MB

Now, that's a lot of text for 4 actionable lines, so let me explain it a little. "$aFILE_TYPE" is the collection of file types you want in your collection. This depends, in large part, on what you MP3 player will handle. Mine will handle ogg and flac, but if yours doesn't, you might change this line to something more like "array("mp3");".

"$aSOURCE_DIR[]" is an array of arrays containing source directories, and the weights you would assign to them. You can add a "$aSOURCE_DIR[]" line for each directory/weight combination, and they will be automatically assigned the appropriate index number. The integer for weight simply indicates how many times songs from a specific source will appear in the catalog of songs from which the random selection will be pulled. If you like "EasyListening" songs a bit more than your "DeathMetal", you might do something like this:

$aSOURCE_DIR[] = array("C:\My Music\EasyListening",2);
$aSOURCE_DIR[] = array("C:\My Music\DeathMetal",1);

So you'd still get random songs from both collections. You'd just be twice a likely to get "EasyListening" than "DeathMetal".

"$sTARGET_DIR" is just where the final files are copied. It doesn't have to already exist, but it will need to be writable.

"$iMEGABYTES" is the total size, in MegaBytes, of the space you want to fill. The script will keep grabbing songs until the next song goes over this limit, so you'll get slightly less than this.

Now to the fun stuff.

// recursive directory crawl used to build list of all available files
function filesArray($sDir,$aFiles){
        global $aFILE_TYPES;
        global $iAllFilesSize;
        // open and read the directory
        $oCurrDir = opendir($sDir);
        // loop through all directory objects
        while (false !== ($sItem = readdir($oCurrDir))) {
                $sFullItem = $sDir . $sItem; // directory + name
                // if it's a directory, excluding the dot alias'
                if (is_dir($sFullItem) && ($sItem != "." && $sItem != "..")){
                        // then perform spider recursively
                        $aFiles = filesArray($sFullItem."\\",$aFiles);
                }
                // if it's a file and has the right estension
                if (is_file($sFullItem)
                        && in_array(pathinfo($sItem, PATHINFO_EXTENSION),$aFILE_TYPES)){
                        // then add to the array
                        $aFiles[] = $sFullItem;
                        // and increment total file size
                        $iAllFilesSize = $iAllFilesSize + filesize($sFullItem);
        }
        }      
        // close directory
        closedir($oCurrDir);
        return $aFiles;    
}

This is the recursive function that crawls through all of your music sources, looking for for files of the specified types, and adding them to collection of available files. The copious internal comments explain pretty much what is happening at each point. One key feature here is the exclusion of the "." and ".." directories, without which the recursive function would spider self-referential directories until you stopped paying your power bill. The "$iAllFilesSize" variable here is used to make sure you're not trying to get more files than you actually have - another recipe for a loop that wont close.

// Test target directory
$bGo = true; // variable used to toggle a hard stop
$iAllFilesSize = 0; //global variable for size of full catalog
if(!is_dir($sTARGET_DIR)){
        if(!mkdir($sTARGET_DIR)){ // try to make the directory
                echo "!!Target directory " .$sTARGET_DIR . " does not exist!!\n";
                $bGo = false; // this should stop the rest of the program
        }else{
                echo "Creating target directory " .$sTARGET_DIR."\n";
        }
}

This next section is just making sure that the directory you want to write to exists. If not, it tries to create it. If that doesn't work, like if the directory is write protected, we flag a global "fail" boolean and skip most of the rest of the script.

// Loop through all source directories, building an array of files matching the types criteria
echo "Building catalog of eligible files in source directories\n";
$aFiles = array();
$aAllFiles = array();
foreach ($aSOURCE_DIR as $aItem) {
        global $aAllFiles;
        $aCurrFiles = array(); // current spider set
        $sRootDir = $aItem[0] . "\\"; // directory to spider
        $iWeight = $aItem[1]; // weight of this source directory
        // Create an array of all files matching the search criteria
        if(!is_dir($aItem[0])){ // first make sure valid source
                echo "!!!Source directory " .$aItem[0] . " does not exist!!!\n";
                }else {
                if($bGo){ // if there is no other reason to stop
                        echo "Spidering \"$aItem[0]\" with a weight of $aItem[1]\n";
                        // call the recursive crawl function
                        $aCurrFiles = filesArray($sRootDir,$aFiles);
                        // weight the source
                        for($i=0;$i<=$iWeight-1; $i++){
                                $aAllFiles = array_merge($aAllFiles,$aCurrFiles);
                        }
                }
        }
}
$iMax = count($aAllFiles)-1; // max boundrary for random selection
echo "Full catalog is ". number_format($iAllFilesSize/1024/1024) . " MBs\n";

Here, we are looping through the array(s) of source file directories, making sure they exist, and calling the recursive function defined above. Again, the inline comments are fairly self-explanatory. One section to note is the "weight the source" clause, where the collection created a specific source, is merged into the full collection as many times as is specified in the weight variable. Note that duplicate files, by full location and name, are excluded in the final selection, so duplicates only increase the likelihood of a file being selected later.

// Confirm universe is bigger than requested selection size
$iMaxBytes = $iMEGABYTES * 1024 * 1024;
if($iAllFilesSize<$iMaxBytes){
        echo "!!!The requested sample is larger than the listed source files!!!\n";
        $bGo = false;
}

Here, we're just making sure you have enough music to deliver your requested Megabytes.

// find the maximum bytes to copy
echo "Building random selection of files matching set criteria\n";
$iCurr = 0;
$bLoop = true;
// create an array containing the files to be copied
$aSelection = array();
while($bLoop && $bGo){
        // pick a random array index out of all possible
        $iIndex = mt_rand(0,$iMax);
        // insert the file into the 'selected array' if you still have space
        if ($iCurr + filesize($aAllFiles[$iIndex]) < $iMaxBytes){
                // and it's not already there
                If(!in_array($aAllFiles[$iIndex],$aSelection)){        
                        $aSelection[] = $aAllFiles[$iIndex];   
                        $iCurr = $iCurr + filesize($aAllFiles[$iIndex]);
                }
        } else {
                // end loop if MB setting met
                $bLoop = false;
        }
}

In this section, we're looping through the universe of all the available\eligible files, and randomly selecting the ones that will be copied over to your target directory. This is done by using "mt_rand" to pick a number between "0", the 1st file in the "$aAllFiles" array, and "$iMax", the last. As with any random function, the exact degree of true randomness is a topic of heated debate between PhDs with strong opinions on the topic, but it's close enough for our purposes.

echo "Copying ". number_format($iCurr/1024/1024) ." of a possible ".  number_format($iMaxBytes/1024/1024) . " MBs to target folder\n";
// Loop though selections array, copying the files to the target
$iCurr = 0; //reset the filesize counter
foreach ($aSelection as $sFile) {
        global $sTARGET_DIR;
        global $iCurr;
        $sNewfile = $sTARGET_DIR . "\\" . basename($sFile);
        if (!copy($sFile, $sNewfile)) {
            echo "!!Failed to copy $sFile!!\n";
        }else{
                $iCurr = $iCurr + filesize($sNewfile);
        }
}

Here, we're looping through the random selection and copying files to the target directory. If a file can't be copied, if it's in use or was just deleted, for instance, we throw an error to the screen to explain why fewer MBs than were requested showed up.

And, finally, we output a message to let the end-user know their files are ready.

// Return a success message when completed.
echo number_format($iCurr/1024/1024) ." MBs copied\n";
echo "The script has finished\n";

To use the script, you must first install the PHP binaries. Windows users should use the latest MSI from here. A reboot might be necessary to get the path to register. On Linux, I would run something like "apt-get install php5". Then open a command line and type "PHP <file location>\randommp3.php". You'll see something like this output to the screen:

C:\Users\dziemecki>php C:\My Music\FavSong\randommp3.php
Creating target directory C:\My Music\Portable
Building catalog of eligible files in source directories
Spidering "C:\My Music\FavSong" with a weight of 1
Full catalog is 162,551 MBs
Building random selection of files matching set criteria
Copying 6,139 of a possible 6,144 MBs to target folder
6,139 MBs copied
The script has finished

I could play with this thing forever, adding features like the weighting, which was a last second whim, and maybe something with ID3 tags or a playlist builder. But that's what edits are for. If you could use something like this, I've uploaded the source code in a text file. Just download and change the extension to PHP. If you run into any problem, let me know. I've been running it for about a week and I've fixed everything I encountered, or even imagined, but there's no lab like the real world. Have at it!

AttachmentSize
Plain text icon randommp3.php_.txt5.25 KB
Tags: