Reading MP3 ID3 tags with ColdFusion (2)

So, almost a month ago I wrote about reading MP3 information using ColdFusion. I promised a follow up the next day and ended up getting a bit busy with a new job, trips, etc. The sad thing is that I wrote the code a few days afterwards and it ended up being exceptionally simple. So - first off - sorry for the delay. Let's look at the CFC I ended up with. As you can see, it is so simple I can share all the code right here:

<cfcomponent displayName="MP3" hint="Reads ID3 information from an MP3" output="false">

	<cfset variables.filename = "">
	<cfset variables.loaded = false>
	<cfset variables.id3tag = "">
	
	<cffunction name="init" access="public" returnType="mp3" output="false">
		<cfargument name="filename" type="string" required="false">
		
		<!--- create an instance of the java code --->
		<cfset variables.mp3 = createObject("java", "org.farng.mp3.MP3File")>

		<cfif structKeyExists(arguments, "filename")>
			<!--- read it in --->
			<cfset variables.filename = arguments.filename>
			<cfset read(variables.filename)>			
		</cfif>
		
		<cfreturn this>
	</cffunction>
	
	<cffunction name="checkLoaded" access="private" returnType="void" output="false"
				hint="Helper function to throw error if no mp3 loaded.">
		<cfif not variables.loaded>
			<cfthrow message="You must first read in an MP3!">
		</cfif>
	</cffunction>

	<cffunction name="getAlbumTitle" access="public" returnType="string" output="false"
				hint="Returns the album title.">
		<cfreturn variables.id3tag.getAlbumTitle()>
	</cffunction>

	<cffunction name="getSongGenre" access="public" returnType="string" output="false"
				hint="Returns the song genre.">
		<cfreturn variables.id3tag.getSongGenre()>
	</cffunction>
	
	<cffunction name="getSongTitle" access="public" returnType="string" output="false"
				hint="Returns the song title.">
		<cfreturn variables.id3tag.getSongTitle()>
	</cffunction>

	<cffunction name="getTrackNumber" access="public" returnType="string" output="false"
				hint="Returns the song title.">
		<cfreturn variables.id3tag.getTrackNumberOnAlbum()>
	</cffunction>

	<cffunction name="getYearReleased" access="public" returnType="string" output="false"
				hint="Returns the song's release date.">
		<cfreturn variables.id3tag.getYearReleased()>
	</cffunction>
	
	<cffunction name="hasID3V1" access="public" returnType="boolean" output="true"
				hint="Returns true if the mp3 has id3v1 information.">
		<cfset checkLoaded()>

		<cfreturn variables.mp3.hasID3v1Tag()>
	</cffunction>

	<cffunction name="hasID3V2" access="public" returnType="boolean" output="false"
				hint="Returns true if the mp3 has id3v2 information.">
		<cfset checkLoaded()>
		
		<cfreturn variables.mp3.hasID3v2Tag()>
	</cffunction>
	
	<cffunction name="read" access="public" returnType="void" output="false">
		<cfargument name="filename" type="string" required="true">

		<!--- does the file exist? --->	
		<cfif not fileExists(arguments.fileName)>
			<cfthrow message="#arguments.fileName# does not exist.">
		</cfif>

		<!--- copy to global scope --->
		<cfset variables.filename = arguments.filename>
		
		<cftry>
			<cfset variables.mp3.init(variables.filename)>
			<cfset variables.loaded = true>
			
			<cfif hasID3V1()>
				<cfset variables.id3tag = variables.mp3.getID3v1Tag()>
			</cfif>
			<cfif hasID3V2()>
				<cfset variables.id3tag = variables.mp3.getID3v2Tag()>
			</cfif>
			
			<cfcatch>
				<cfthrow message="Invalid MP3 file: #arguments.filename# #cfcatch.message#">
			</cfcatch>
		</cftry>
	</cffunction>
	
</cfcomponent>

So - first - a recap. In the last entry I talked about the Java ID3 Tag Library. This is the open source project that I'm wrapping with ColdFusion. ID3 tags are the encoded information in the MP3 file that tslks about the song. It contains different bits of information based on the style of ID3 tag used in the file. There are two main version of ID3, and various sub versions of each. The Java ID3 Tag Library supports working with both main styles of ID3 tags and has specific API calls to work with them.

But - and this is why I love the project so much - the author also wrote a set of simple methods that will work with any version ID3 tag. In the code above, check out getAlbumTitle and getSongTitle. While I could have used specific API calls for the two versions of ID3, I didn't have to since there were generic functions built into the code.

To be honest, I got lucky. This was one of the first Java libraries I found, and it just turned out to be darn easy and useful. So how could you use this? You can imagine a site that let's users upload mp3s. (Legal of course.) Instead of asking the user to enter information about the song, you can use ColdFusion to read out all the ID3 information automatically.

Anyway - let me know if you actually use this on a production site. I'd be curious to see it in use.

Archived Comments

Comment 1 by dickbob posted on 7/10/2006 at 2:17 PM

Interesting posting Ray.

I'm working on a project where I need to write ID3 tags to a MP3 file as a simple form of copyright protection. The idea is that the file would be stamped with the person who downloaded the file. Not fool proof I know but a cheap form of DRM.

How easy do you think it would be to extend your cfc to offer wrting methods?

dickbob

Comment 2 by Raymond Camden posted on 7/10/2006 at 3:20 PM

His library does support writing. I'd suggest going to his site and checking the API out. It isn't that hard.

Comment 3 by Raymond Camden posted on 7/10/2006 at 3:21 PM

Check his quick start too. Notice the simple API like setSongTitle(), and then save(). Very trivial.

Comment 4 by Phillip Senn posted on 7/10/2006 at 5:12 PM

I would have made the "read" function returntype="string" and did a <cfreturn result>, with result being either getID3v1Tag or getID3v2Tag.

OK, I'm a little slow on the uptake.
How do we use this component again?
So far I've got:

<cfset mp3Obj = createObject("Component", "mp3")>
<cfset dir = "c:\inetpub\wwwroot\mymusic\">
<cfdirectory name="music" action="list" directory="#dir#" filter="*.mp3">
<cfloop query="music">
<cfoutput>
Filename = #name#<br />
Album Title = #mp3Obj.getAlbumTitle#<br />
</cfoutput>
</cfloop>

Do I call the read function?

Comment 5 by Raymond Camden posted on 7/10/2006 at 5:20 PM

Read is my way of saying load, so I think void makes sense.

Phillip, check the last entry. I believe I attached a simple test script that.

Comment 6 by Phillip Senn posted on 7/10/2006 at 5:25 PM

I reread the last entry (dated June 13, 2006) but couldn't find a test script. So then I reread the last entry before this one, but it was about Upcoming Speaking Engagements.

Comment 7 by Raymond Camden posted on 7/10/2006 at 5:30 PM

Ok, here is my test script. Forgive any freaky formatting.

<cfset mp3 = createObject("component", "mp3").init()>

<cfset dir = "g:\music\80s\">

<cfdirectory action="list" directory="#dir#" filter="*.mp3" name="music">

<cfloop query="music">
<cfoutput>filename = #name#<br></cfoutput>

<cftry>
<cfset mp3.read(dir & name)>
<cfoutput>
has v1? #mp3.hasid3v1()#<br>
has v2? #mp3.hasid3v2()#<br>
title: #mp3.getSongTitle()#<br>
album: #mp3.getAlbumTitle()#<br>
genre: #mp3.getSongGenre()#<br>
track: #mp3.getTrackNumber()#<br>
year: #mp3.getYearReleased()#<br>
</cfoutput>
<cfcatch>
bad file <cfoutput>#cfcatch.message#</cfoutput>
</cfcatch>
</cftry>
<hr>
</cfloop>

Comment 8 by Jeff Lemmon posted on 7/11/2006 at 10:26 AM

Many thanks for knocking this out so quickly. I did have one question. I'm getting the message:

Class not found: org.farng.mp3.MP3File

I assume I'm supposed to download the Java files from http://javamusictag.sourcef... and map the component to those files? Any ideas on how this might work in a shared environment where I have no ability to load these via the CF administrator? Sorry for the dumb question, but my CF skills aren't what they used to be.

-Jeff

Comment 9 by Raymond Camden posted on 7/11/2006 at 5:18 PM

Jeff, yes, please read the first entry as I think it makes it a bit more clear. On a shared environment, you would need to contact tech support.

Comment 10 by Gideon Marken posted on 7/12/2006 at 4:19 AM

Hi Ray - I was considering using this on http://www.ArtistServer.com - which is a site running on ColdFusion. I'm currenlty hosting 5,800 mp3s, and have 15,000 registered members. Your CFC didn't include a few things I need (bitrate, channel mode, frequency, and sample rate) so I looked in the API docs and found the methods for these settings.

Unfortunately, my modification either didn't work, or these methods are not complete in in the Java Library.

Before I try to seek help with the Java Library, I wanted to make sure there wasn't anything wrong with my CF code? Could you give this a quick look over? :)

I added this after your "checkLoaded" function:
==================================================

<!--- MP3 File Attributes --->
<cffunction name="getMp3BitRate" access="public" returnType="string" output="false"
hint="Returns the mp3 bit rate.">
<cfreturn variables.mp3.getBitRate()>
</cffunction>
<cffunction name="getMp3Frequency" access="public" returnType="any" output="false"
hint="Returns the mp3 frequency.">
<cfreturn variables.mp3.getFrequency()>
</cffunction>
<cffunction name="getMp3Mode" access="public" returnType="string" output="false"
hint="Returns the mp3 channel mode.">
<cfreturn variables.mp3.getMode()>
</cffunction>
<cffunction name="getMp3Version" access="public" returnType="string" output="false"
hint="Returns the mp3 Mpeg version.">
<cfreturn variables.mp3.getMpegVersion()>
</cffunction>
<!--- MP3 File Attributes --->

This is the API documentation page with these methods:
http://javamusictag.sourcef...

This is what I get back for a
stereo 44100 128k mp3 file:
======================

bitrate: 0
frequency: cfmp32ecfc1980346658$funcGETMP3FREQUENCY@6df9bc
channel mode: 0
mpeg version: 0

===================
And this is what I added to the demo code, after 'year':

<div style="background-color:##F5F5F5">
bitrate: #mp3.getMp3BitRate()#<br />
frequency: #mp3.getMp3Frequency#<br />
channel mode: #mp3.getMp3Mode()#<br />
mpeg version: #mp3.getMp3Version()#
</div>

Thank you for your time and your effort on this code.

- Gideon

Comment 11 by Raymond Camden posted on 7/12/2006 at 5:40 AM

You are using the wrong variable. You want to use variables.id3tag. Notice how my other methods did.

Comment 12 by Michael White posted on 7/12/2006 at 7:35 AM

this is a lame question:
ok, so all my mp3's are on another server... how do (or can I) use cfdirectory on another server's file system? I know this would be crazy on a production environment but just on my home servers so i can play around

Comment 13 by Raymond Camden posted on 7/12/2006 at 5:14 PM

You need to run ColdFusion as a user who has access to that other server and then make a mapped drive for it.

Comment 14 by Jeff Lemmon posted on 7/13/2006 at 3:26 AM

Does anyone know how to read a MP3 duration using this CFC?

Comment 15 by Anthony Webb posted on 8/2/2006 at 6:20 AM

On the surface it looks like adding some other fields and functions to the CFC would be a real no brainer but I am having a devil of a time trying to get songlength, bitrate, genre etc! Can someone post a sample or a revised cfc or even directions on how in the world you can pull some more info from the ID3 tag?

Thanks, and BTW this is a pretty cool cfc, I have been looking everywhere for this.

Comment 16 by Raymond Camden posted on 8/2/2006 at 6:24 AM

What is going wrong? Also, if you are storing the CFC in the app scope, don't forget to refresh it.

Comment 17 by Anthony Webb posted on 8/3/2006 at 2:20 AM

It must be the whole app scope thing, how do I refresh that?

Comment 18 by Anthony Webb posted on 8/3/2006 at 2:26 AM

When I call the mp3.getMp3BitRate() I get the following repeated all the way down the page.

bitrate: bad file The selected method getBitRate was not found

Comment 19 by Anthony Webb posted on 8/3/2006 at 2:27 AM

Sorry, and my function is:

<cffunction name="getMp3BitRate" access="public" returnType="string" output="false"
hint="Returns the mp3 bit rate.">
<cfreturn variables.id3tag.getBitRate()>
</cffunction>

Comment 20 by Raymond Camden posted on 8/3/2006 at 5:52 PM

If you are caching the creation of the CFC, then you want to stop that whole you test.

Oh - I found your issue. The Mp3 bit rate is NOT a method of the ID3 object. It is a method of the mp3 object.

This _should_ work inside a new getBitRate() method that you would add yourself to the CFC.

<cfreturn variables.mp3.getBitRate()>

Comment 21 by anthony webb posted on 8/3/2006 at 8:46 PM

Ray,

You were spot on for the getBitrate method! My test files show a '0' for bitrate, but that may be an issue with my test files. I am just glad it doesnt error out. Now I am trying to get the length and have hit some issues that are very similar. I have:

<!--- MP3 File Attributes --->
<cffunction name="getMp3BitRate" access="public" returnType="string" output="false"
hint="Returns the mp3 bit rate.">
<cfreturn variables.mp3.getBitRate()>
</cffunction>
<cffunction name="getMp3Length" access="public" returnType="string" output="false"
hint="Returns the mp3 length.">
<cfreturn variables.mp3.getLength()>
</cffunction>
<!--- MP3 File Attributes --->

But the getMP3length returns an error saying the method path was not found. Any ideas on this? I tried out the cfx id3 tag mentioned above which works great and is very very fast but it bombs on some files and your cfc reads them great. Problem is the cfc lacks some prety critical fields for me like length.

I am so close.

Thanks again for dealing with some dumb questions.

Comment 22 by Raymond Camden posted on 8/3/2006 at 8:51 PM

In looking at the API doc for mp3,

http://javamusictag.sourcef...

I do not see a getLength().

Comment 23 by anthony webb posted on 8/3/2006 at 9:03 PM

I saw this:
getLength() - Method in class org.farng.mp3.object.ObjectNumberFixedLength

So I tried:
variables.object.getLength()

But that didnt work either :( I'm sure I am way off, I just have never dealt with java before so reading these docs is greek to me.

-Anthony

Comment 24 by Raymond Camden posted on 8/3/2006 at 10:36 PM

You got me there. I don't see a way to get the object class from the Mp3.

Comment 25 by Jeff Lemmon posted on 8/8/2006 at 5:28 AM

I found a java class called helliker that can pull the duration from an MP3. I actually found it when I downloaded the wimpyplayer:

http://www.wimpyplayer.com/...

Get the demo CF version and extract the helliker directory, placing it into C:\CFusionMX7\wwwroot\WEB-INF\classes. Restart CF. Then:

<CFOBJECT type="JAVA" action="CREATE" name="MP3File" class="helliker.id3.MP3File">
<CFSET ret = MP3File.init("C:\MP3\test.mp3")>
<CFOUTPUT>
Exists? #MP3File.id3v1Exists()#<BR>
Is MP3? #MP3File.isMP3()#<BR>
File Size? <CFSET sizeMB = Round(((MP3File.getFileSize()/ 1024)/1024))>#sizeMB# MB<BR>
File Name? #MP3File.getFileName()#<BR>
Play Time? #MP3File.getPlayingTimeString()#<BR>
Bitrate? #MP3File.getBitRate()#<BR>
</CFOUTPUT>

I pulled this little gem from here :-)

http://www.houseoffusion.co...

Comment 26 by Jeff Lemmon posted on 8/8/2006 at 7:44 AM

Here are the methods for helliker:

http://www.jukex.org/docs/a...

Comment 27 by Anthony Webb posted on 8/25/2006 at 12:28 PM

Ya! Worked great for me, thanks for the tip man!

Comment 28 by Lance posted on 12/19/2006 at 1:09 AM

Hi, Ray. I am trying to implement this on my server and am having problems returning the Genre for just some .mp3s. This doesn't happen to all, but some show like (17) for Rock, etc. The ID3 data looks fine everywhere else...I don't know what to do for this. Anyone else experienced this?

Thanks,
Lance

Comment 29 by Raymond Camden posted on 12/19/2006 at 2:20 AM

If I remember the spec right - Genres _are_ numbers, or maybe they were for the older ID3 spec. So 1=Country,2=Rap, etc. You may be seeing that.

Comment 30 by Riyaz posted on 1/17/2008 at 4:44 PM

@ Jeff Lemmon

Wow, great post man, i have been looking for this and infact i am torturing the clients to add the mp3 file duration themself, lol,
really you made my day :) thumbs up , million stars

Comment 31 by Writing to an MP4 posted on 8/7/2008 at 11:03 AM

Has anyone out there had any luck figuring out how to write. I looked at the API documentation but I'm having a hard time figuring out where to even start. If anyone can help me out that would be awesome.

Comment 32 by simspace posted on 10/15/2008 at 9:50 PM

I was not able to figure out how to write with this java library. However, I found a great ID3 library named MyID3 at http://www.fightingquaker.c.... And it worked the first time!. Very easy to use you will just need to download the Jakarta Regexp and Nanoxml jars as well.

Head over to my blog (http://flexdojo.blogspot.com/) for a simple mp3Manager cfc that uses the MyID3 library.

Comment 33 by Adrian Lynch posted on 10/16/2008 at 4:58 PM

Is this a mistake maybe?

If you pass the filename into via the init method, you don't set VARIABLES.filename before attempting to use it in the read() method.

So:

<cfif structKeyExists(ARGUMENTS, "filename")>
<!--- read it in --->
<cfset read(VARIABLES.filename)>
</cfif>

should be:

<cfif structKeyExists(ARGUMENTS, "filename")>
<!--- read it in --->
<cfset VARIABLES.filename = ARGUMENTS.filename>
<cfset read(VARIABLES.filename)>
</cfif>

Comment 34 by Raymond Camden posted on 10/16/2008 at 10:56 PM

Certainly is. I'll edit the blog entry now. Thanks Adrian.

Comment 35 by amclean posted on 10/9/2009 at 3:49 AM

I wondered if you or anyone could explain how to add song *duration* to the class. I read through the documentation but I have a bit of trouble understanding anything java.

Comment 36 by Lee Boardman posted on 6/1/2010 at 7:29 PM

Can somebody help me out with extracting 'comments' from an MP3 file. The original post by Ray included this but with that code the 'title' was only returning 30 characters. Using the CFC on this post I can now get the full title but am unable to get the comments... Thanks in advance

Comment 37 by Raymond Camden posted on 6/1/2010 at 7:31 PM

Lee - sorry, which CFC? It sounds like you said my code doesn't get the title right but does do comments, but then some other CFC does title right and not comments. Which CFC?

Comment 38 by Lee Boardman posted on 6/1/2010 at 7:59 PM

Ray, sorry for any confusion. What I mean is, when I run the code on your original post I see the comments for all MP3s, but the output for the title weirdly only shows the first 30 characters.

I'm now running the CFC on this second post, along with the example code posted in a comment by you on 10 July 2006, at 8:30 AM (a long time ago, I appreciate!) and the full title is being displayed, which is great! However, I can't see any way of extracting the comments by using this CFC.

Hope that makes sense and thanks for the quick reply!

Comment 39 by Raymond Camden posted on 6/1/2010 at 8:01 PM

It does make sense. I'll try to tackle this @ lunch.

Comment 40 by Raymond Camden posted on 6/1/2010 at 9:03 PM

Add this to the CFC:

<cffunction name="getSongComment" access="public" returnType="string" output="false"
hint="Returns the song title.">
<cfreturn variables.id3tag.getSongComment()>
</cffunction>

That's all you need. Surprised this library still works after 4 years. ;)

Comment 41 by Raymond Camden posted on 6/1/2010 at 9:03 PM

Add this to the CFC:

<cffunction name="getSongComment" access="public" returnType="string" output="false"
hint="Returns the song title.">
<cfreturn variables.id3tag.getSongComment()>
</cffunction>

That's all you need. Surprised this library still works after 4 years. ;)

Comment 42 by Lee Boardman posted on 6/3/2010 at 12:56 PM

Thanks Ray. I added this to the CFC and tried it out. The MP3 I tested it out on had a comment of 'purina.jpg' (the client stores logo info in the MP3 meta-data - the idea being that this info can be extracted and then used to display a logo). The code on your original post pulls this fine but with the addition to the CFC it pulls this -

cfmp32ecfc95810042$funcGETSONGCOMMENT@144b1056

I've got no idea where that comes from ?!

Comment 43 by Raymond Camden posted on 6/3/2010 at 3:10 PM

You did use parens, ie, getSongComment(), in your code, right? Instead of getSongComment.

Comment 44 by Lee Boardman posted on 6/3/2010 at 3:42 PM

Ooops! I've put them in. Now it's not displaying anything

Comment 45 by Raymond Camden posted on 6/3/2010 at 3:44 PM

Then you are out of luck. :) If the Java app doesn't see that comment, then ... it doesn't work right with that mp3.

Although wait - does it work with the pre-CFC code?

Comment 46 by Ronnie Duke posted on 2/24/2014 at 5:11 AM

Hey Ray,

Do you know if this is limited to mp3, or could this theoretically work for all id3 tags (aac, m4p, etc)?

Comment 47 by Raymond Camden posted on 2/24/2014 at 7:11 AM

Did you try? :)

Comment 48 by Ronnie Duke posted on 2/24/2014 at 7:12 AM

I will now :-)

I was just seeing if anyone else had done it before before I went down a dead end.

Comment 49 by Michael G Kear posted on 9/10/2017 at 10:43 AM

Ray, this is quite old now, but I have been using it since you wrote it. And it's proved very useful. I"m webmaster for a radio station and of course we do a LOT of work with music files, mostly MP3s. I'm using this CFC with a few tweaks for the following:
Library maintanance: When we load new files into our library drive, I scan through the drive periodically and read in details of any mp3s i havent indexed and load the details into the SQLServer database. It's 90% reliable - the errors are related to mp3s not being properly tagged rather than issues with the cfc.
Program generation - I have a number of scripts that scan through the hard drive pulling out details of mp3s we havent played before, or are new, or are of a particular genre to make up program playlists.
On-air assistance for show hosts - I display the song currently being played or next on the list for the host so he has something to say about the song
Timing shows: when a show is designated to be a 3 hour show, that means 3 hours, plus or minus 1 second. When the show is coming up to a commitment like the end of the show or a news satellite link or an ad break, its important to time it correctly. You can't do that before the show starts because you dont know exactly how many seconds the host is going to speak for in the hour. So about 10-15 minutes to the commitment the host clicks on a link and a script calculates which songs he should play to time up to the commitment with about 5 seconds to spare for the host to speak, e.g. "News sport and weather is next, then lots more music. Stay tuned!"
Thank you for all the many contributions you made to us ColdFusion developers in your Adobe days. I just wanted you to know that it's still useful.

Mike Kear,
Windsor, NSW, Australia.

Comment 50 (In reply to #49) by Raymond Camden posted on 9/11/2017 at 1:09 PM

Thank you for sharing this. :)

Comment 51 (In reply to #50) by Raymond Camden posted on 9/11/2017 at 1:17 PM

FYI, I'm pushing up a fix to the code formatting.