Page 1 of 2

using php to auto-generate an m3u playlist?

Posted: Tue Oct 11, 2005 7:50 pm
by jxn
alright... I need some help figuring out if this is possible/worth-figuring-out-how to do with php...

I basically need to generate m3u files to be used for playlists approximately 24 hours long. All the music files are in a directory (some are in subdirectories, but if absolutely necessary, I can move them all into the same directory), in both ogg vorbis and mp3 format. However, I also have a directory with wav/speex/flac/ogg/mp3 "announcements" that need to occur every so often, mixed in with the music. I basically want a random music playlists generated, with random audio announcements every 20 minutes (I *could* estimate and say every 4 songs or so, but I want to avoid the announcements becoming too frequent if the playlist happens to be mostly short songs or vice versa).

I am aware of a php-id3 lib that can read the file time lengths and metadata (required for the m3u files)... but I'd just like some opinions on whether or not this will be easy/worth-my-time to do with php or should I try a different language entirely.

The only reason I haven't already started it is because I'm not sure how well this random-file-opening/selecting from thousands of files in a single directory will work, but I'd prefer to use php if possible because I might eventually database all the file names/track-lengths and turn the program into a web-application, which would be easiest if I worked with php. Plus, I've had a little, itty-bit of php experience, which is more than I can say for, say, python or perl.

Any help or tips on whether I should do this with php or how I might go about selecting random files from a directory?

Thanks!

Posted: Tue Oct 11, 2005 8:32 pm
by feyd
generating an m3u list is fairly easily since it's a very basic text file.. however extracting metadata from a ID3v2 file isn't exactly easy, as compared to a ID3v1.. in the end, it's probably best to use an existing library that can read (at least) ID3v2, among the other metadata formats you are working with. It would be best to cache the metadata (at least the essentials) in a database to speed creation, checking each file's hash to make sure it's the same file your metadata is associated with (in case you update the metadata in the file).

Posted: Tue Oct 11, 2005 9:30 pm
by jxn
yes, I've discovered the getid3 library, which appears to be quite comprehensive and offers more or less what I need... though I've yet to figure out how exactly to read the contents of a directory and randomly read a file a few hundred times with any speed... would caching the entire contents of the directory before making the random searches likely speed this up dramatically? I'm not actually sure if I'm going about using the library correctly at this point.

Posted: Tue Oct 11, 2005 9:51 pm
by Burrito
I very recently did a project doing almost exactly what you're after.

I wrote a function and used the getid3 lib to scan and traverse sub directories of all my music files then dump the data into a mysql db.

so searching for files happens very quickly as it just queries the db to search data.

I then wrote another script to generate my m3u files and send them down to the client to play the playlist off of the server.

if you're interested, I will post the code tomorrow....

Posted: Tue Oct 11, 2005 11:08 pm
by jxn
Burrito wrote:if you're interested, I will post the code tomorrow....
in fact, nothing would make me happier to see a working implimentation of this!!!

while I was going to attempt to try doing this without a db (merely for the sake of simplicity/portability, I actually have mysql installed on all three of the machines I'll be using this script on for now), a mysql-utilization version is probably significantly faster for the number of tracks (1000+) that I am working with anyway.

Are you using getid3 or a similar library?

Thanks again; I'm very eager to see your implimentation(s)!

Posted: Wed Oct 12, 2005 10:40 am
by Burrito
sry it took me so long, had a rough commute this morning :S

Code: Select all

<?php 
set_time_limit(0);
mysql_connect("localhost","username","password")
	or die(mysql_error());
mysql_select_db("database")
	or die(mysql_error());
require_once('getid3/getid3.php');
$filearr = array();
function recurseDir($dir)
{
	// create array of accepted file types...burrito
	$acceptedfiles = array('mp3','m4a','wma','wav','ogg','wmv','asf');
	// instantiate new object for getid3...burrito
	$getID3 = new getID3;
	if ($handle = opendir($dir)) 
	{
		// loop the current directory...burrito
	   while (false !== ($file = readdir($handle))) 
	   {
	   		// check to see if item is "." or ".." or is a sub directory...burrito
	       if($file != '.' && $file != '..' && !is_dir($dir.$file))
		   {
		   		// get file extension and ensure it's an accepted file type...burrito
		   		$ext = explode(".",$file);
				$ext = array_pop($ext);
				$ext = strtolower($ext);
				if(in_array($ext,$acceptedfiles))
				{
					// get file size...burrito
					$size = filesize($dir.$file);
					// grab id3 etc data from file...burrito
			   		$ThisFileInfo = $getID3->analyze($dir.$file);
					// use copytagstocomments static method to push all id3 info to comments_html array...burrito
					getid3_lib::CopyTagsToComments($ThisFileInfo);
					// set all local vars...burrito
					$title = (isset($ThisFileInfo['comments_html']['title'][0]) ? $ThisFileInfo['comments_html']['title'][0] : "");
					$artist = (isset($ThisFileInfo['comments_html']['artist'][0]) ? $ThisFileInfo['comments_html']['artist'][0] : "");
					$bitrate = ($ThisFileInfo['audio']['bitrate'] /1000);
					$bitrate = explode(".",$bitrate);
					$bitrate = $bitrate[0]."k";
					$playtime = $ThisFileInfo['playtime_string'];
					$publisher = (isset($ThisFileInfo['comments_html']['publisher'][0]) ? $ThisFileInfo['comments_html']['publisher'][0] : "");
					$genre = (isset($ThisFileInfo['comments_html']['genre'][0]) ? $ThisFileInfo['comments_html']['genre'][0] : "");
					$track = (isset($ThisFileInfo['comments_html']['track'][0]) ? $ThisFileInfo['comments_html']['track'][0] : "");
					$album = (isset($ThisFileInfo['comments_html']['album'][0]) ? $ThisFileInfo['comments_html']['album'][0] : "");
					$year = (isset($ThisFileInfo['comments_html']['year'][0]) ? $ThisFileInfo['comments_html']['year'][0] : "");
					$composer = (isset($ThisFileInfo['comments_html']['composer'][0]) ? $ThisFileInfo['comments_html']['composer'][0] : "");
					// insert into the db...burrito
					$query = "insert into mp3
					(
					title,
					artist,
					bitrate,
					playtime,
					publisher,
					genre,
					track,
					album,
					year,
					composer,
					filename,
					filepath,
					filenamepath,
					dateentered,
					filesize
					)
					values
					(
					'".mysql_escape_string($title)."',
					'".mysql_escape_string($artist)."',
					'".mysql_escape_string($bitrate)."',
					'".mysql_escape_string($playtime)."',
					'".mysql_escape_string($publisher)."',
					'".mysql_escape_string($genre)."',
					'".mysql_escape_string($track)."',
					'".mysql_escape_string($album)."',
					'".mysql_escape_string($year)."',
					'".mysql_escape_string($composer)."',
					'".mysql_escape_string($ThisFileInfo['filename'])."',
					'".mysql_escape_string($ThisFileInfo['filepath'])."',
					'".mysql_escape_string($ThisFileInfo['filenamepath'])."',
					now(),
					'$size'
					)
					";
					mysql_query($query)
						or die(mysql_error());
				}
				
				
		   }
		   // if item is a directory re-run the function to deep scan and grab lower level files...burrito
		   else if(is_dir($dir.$file))
		   {
		   		if($file != "." && $file != "..")
				{
					$subDir = $dir.$file;
					//echo $subDir;
			   		recurseDir($subDir."/");
				}
		   }
	   }	
	   closedir($handle); 
	}
	
}
// call function on initial run...burrito
if(isset($_POST['dir']))
{
	$list = recurseDir($_POST['dir']);
	echo "done!";
}
else
{
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>
<head>
	<title>Enter Folder To Add To Database</title>
<style>
input
{
background-color:#b7d0dd; border:1px #000000 solid; font-family:arial; font-size .84em;
}
body
{
font-family:arial; font-size .84em;
}
</style>
</head>

<body>
<form method="post">
Enter Folder:<br>
(use forward slashes and make sure you have a trailing slash on the end ex: c:/path/to/files/ )<br>
<input type="text" name="dir"><br>
<input type="submit" value="Add Files To DB">
</form>


</body>
</html>
<?
}

?>

thanks!

Posted: Wed Oct 12, 2005 6:37 pm
by jxn
Thanks for the loader script, it worked quite well for filling up the DB.

I am curious as to whether, if you still have the script to generate the .m3u file, you might be willing to let me peek at that code also, as it sounds the most similar to the primary task of my script. (also, do you have the db structure for your mp3 table?)

Just in case you're curious about what I'm doing, I'm planning on altering these scripts quite a lot for my purposes... much of the metadata for these files is already stored in a separate database, where the songs are rated based on my preferences/etc, and I'll be using this script to link the existing metadata to the actual filename... and then I hope to eventually generate my m3u with "random" tracks, giving preference to newer and higher-rated tracks. I haven't quite figured out the math of how I'm going to do this yet (especially the weighted random), but this db loading code is an excellent example of how my implimentation will begin, and I'd love to see what your next step was, burrito!

Posted: Wed Oct 12, 2005 6:43 pm
by jxn
also, I noticed that your db includes "filename" "filepath" and "filenamepath", and I'm kind of curious as to the benefit of this. Is there any difference between the output of

Code: Select all

<?php echo($filepath . '/' . $filename ) ?>
and

Code: Select all

<?php echo($filenamepath) ?>
?

Posted: Wed Oct 12, 2005 7:04 pm
by Burrito
I only used all three because I could...like why a dog licks himself I guess:

here is my mp3 table structure:

Code: Select all

CREATE TABLE `mp3` (
  `id` mediumint(9) NOT NULL auto_increment,
  `title` varchar(255) default NULL,
  `artist` varchar(255) default NULL,
  `bitrate` varchar(100) default NULL,
  `playtime` varchar(100) default NULL,
  `publisher` varchar(255) default NULL,
  `genre` varchar(255) default NULL,
  `track` varchar(100) default NULL,
  `album` varchar(255) default NULL,
  `year` varchar(100) default NULL,
  `composer` varchar(255) default NULL,
  `filename` varchar(255) default NULL,
  `filepath` varchar(255) default NULL,
  `filenamepath` varchar(255) default NULL,
  `dateentered` date default NULL,
  `filesize` varchar(100) default NULL,
  PRIMARY KEY  (`id`)
)

here is my playlist table:

Code: Select all

CREATE TABLE `mp3playlists` (
  `id` mediumint(9) NOT NULL auto_increment,
  `playlist` varchar(100) default NULL,
  `username` varchar(100) default NULL,
  `datetime` datetime default NULL,
  `userid` mediumint(9) default NULL,
  PRIMARY KEY  (`id`)
)
and here is my playlistssongs table

Code: Select all

CREATE TABLE `mp3playlistsongs` (
  `id` mediumint(9) NOT NULL auto_increment,
  `songid` mediumint(9) default NULL,
  `playlistid` mediumint(9) default NULL,
  `datetime` datetime default NULL,
  `userid` mediumint(9) default NULL,
  PRIMARY KEY  (`id`)
)

below is the code I use to create my playlists, there are a few things hard coded on the page that you'll need to change and as you'll see in my comments, I didn't account for songs under a minute or over an hour, but that shouldn't be too hard to adapt. I use XMLHttp to request my list file name so this script in and of itself won't accomplish everything for you. You could always just use a redirect to point to the m3u file at the end though. I also defined a couple of constants (one for the drive path and one for the internet URL) which you'll see in my str_replace, just change those to suit you.

Code: Select all

<?
require_once("includes/dbInteract.php");
$id = $_POST['listnum'];
$query = "select * from mp3playlistsongs where playlistid = $id";
//echo $query;
$result = mysql_query($query)
	or die(mysql_error());
//generate random number to append to play list so that browser won't cache old lists...burrito
$bike = rand(10001,99999);
$filename = "playlists/playlist".$bike.".m3u";
//start m3u file...burrito
$file = "#EXTM3U\r\n";
while($row = mysql_fetch_assoc($result))
{
	$song = $row['songid'];
	$getsong = mysql_query("select * from mp3 where id = $song")
		or die(mysql_error());
	$gtsong = mysql_fetch_assoc($getsong);
	// get the length of the song in seconds...still needs work for songs less than one minute or songs over an hour...burrito
	$tm = explode(":",$gtsong['playtime']);	
	$tim = ($tm[0] * 60) + $tm[1];
	$tit = ($gtsong['title'] != "" ? $gtsong['title'] : "Some Song From Burrito");
	$splits = explode("/",$gtsong['filenamepath']);
	$fnamer = "";
	$sz = sizeof($splits);
	for($i=0; $i<$sz; $i++)
	{
		
		$fnamer .= ($splits[$i] == "x:" ? "x:/" : rawurlencode($splits[$i]).($i != $sz -1 ? "/" : ""));
	}
	$loc = str_replace(PATH,HTPATH,$fnamer);
	$file .= "#EXTINF:".$tim.",".$tit."\r\n";
	$file .= $loc."\r\n";
}
   if (!$handle = fopen($filename, 'w')) 
   { 
        print "Cannot open file ($filename)"; 
        exit; 
   } 
   if (!fwrite($handle, $file)) 
   { 
       print "Cannot write to file ($filename)"; 
       exit; 
   } 
   fclose($handle); 
echo $filename;
exit();
?>

Posted: Wed Oct 12, 2005 7:32 pm
by jxn
here's the preliminary code that I'm using to make the .m3u file...just for reference sake...

Code: Select all

<?php
set_time_limit(0);
mysql_connect("localhost","username","password")
    or die(mysql_error());
mysql_select_db("music_db")
    or die(mysql_error());


echo('#EXTM3U<br />');
$max_playlength = 400;
$playlength = 0;
while ($playlength < $max_playlength) {
	$rand_track_sql = mysql_query("SELECT * FROM `mp3` ORDER BY RAND() LIMIT 1 ");
	$track = mysql_fetch_array($rand_track_sql);
	$temp_time = explode(':', $track['playtime']);
	$time = (($temp_time[0] * 60) + $temp_time[1]);
	
	echo('#EXTINF:');
	echo($playlength . '. ' . $time . ', ' . $track['artist'] . ' - ' . $track['title'] . '<br />');
	echo($track['filepath'] . '/' . $track['filename'] . '<br />');
	$playlength++;
}

?>

Posted: Thu Oct 13, 2005 11:47 am
by jxn
burrito, thanks much for the code! In the end, I've written a completely different mysql-table --> m3u playlist, and using a heavily modified version of your loading script (because our needs are quite a bit different), but it was a heck of a lot easier to see examples before I got started; my project is nearly complete, but I bet it would hardly be started without your help, so thanks!!

I'll post my final code later, after I get things "perfected", in case it'll help someone else out...

I have one last question, though, that doesn't necessarily apply to burrito/burrito's code or your code... I'm using "ORDER BY RAND()" in a single mysql statement to retrieve tracks from the db, but I want to have weighted results, giving preference to the track's rating and age if they have them (already put in the db), so that more good tracks and more newer tracks get slightly more play (I'm thinking roughly a 10-20% boost for each).... is there an easy way to do this that anybody knows of? I might also post this question on the mysql-php subforum, after I do some more searching to make sure this hasn't already been asked a few times.

Posted: Tue Nov 08, 2005 6:23 am
by nicodejong
I followed the code exactly as it has been written, and it works.

But when i try to import about 40.000 mp3's into the database, everytime the php script stops after 5 min.. (300 sec)

Allready enlarged the timeout for PHP.INI

Using PHP 4.4.1 on IIS6 with MySQL 4.1.14 on a Windows 2003 Server..

Can anybody help me to solve this...

Posted: Tue Nov 08, 2005 8:10 am
by feyd
that's a lot of files to read, set the timeout to 0 (infinite)

set_time_limit()

Posted: Tue Nov 08, 2005 8:35 am
by Ambush Commander
Be careful, since I know Apache also has a limit on script execution... not so sure about IIS. You might want to try chunking it into smaller pieces.

Posted: Tue Nov 08, 2005 9:51 am
by nicodejong
Tried it with set_time_limit()... was not working.

I found a solution: IIS6, Metabase.XML (CGITimeout = 300) setted it to (CGITimeout = 7200)

And it works!!!