Menu

As simple as possible, as complex as necessary

itunes:duration integer values cause CFFEED to fail

4 April 2011

<cffeed> is a great example of how ColdFusion neatly bundles up a set of complex but commonly required web operations into a simple yet powerful tag (or object if like me you prefer cfscript).

However, I've found an edge case where it doesn't quite pass muster, at least in the current 9.0.1 version.

A client of ours publishes a podcast using libsyn's popular service, and wanted to include an automatically updated link to the latest edition on their home page. Sounds like a job for <cffeed>.

But despite passing various validation tests, <cffeed> refused to process the podcast feed, throwing an "Invalid XML" exception.

A process of elimination pointed to the <itunes:duration> element within each <item> as the offending piece, containing an integer representing the length of the recording in seconds.

<itunes:duration>1154</itunes:duration>

Invalid?

Firstly, despite the error message the above is valid XML. So maybe ColdFusion is really trying to say that it's not a valid value for the iTunes Podcasting namespace's duration element. Well according to the spec, that's not true either:

The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours, M = minutes, S = seconds). If an integer is provided (no colon present), the value is assumed to be in seconds.

Clearly this is a validation problem at ColdFusion's end and I've filed a bug with Adobe in the hope that it gets addressed.

Work-around

Meantime, I had been advising my client to just manually edit their podcast metadata so that the duration was in the MM:SS format and this worked fine. But in the latest version of libsyn's publishing system the ability to override the autogenerated timing value has been removed.

So I've now come up with a CFC which gets round the problem by going back to pre-<cffeed>techniques using <cfhttp> and XML parsing. I've also used it as an opportunity to have a go with CF9's full-component scripting. I've left the enclosing <cfscript> tags for the sake of code-colouring (for which thanks go to John Whish for updating SyntaxHighlighter's CF support to work with the latest version).

// podcastFeedReader.cfc
component displayName="podcastFeedReader" hint="Use me to work around the bug in CFFEED which causes podcast feeds with itunes:duration values in seconds only to be rejected as invalid. I will convert the feed so that these integer values are converted to HH:MM:SS format."
{

	public any function init()
	{
		// create instances of the built-in feed and http objects
		variables.instance	=
		{
			cffeed = New com.adobe.coldfusion.feed()
			,cfhttp = New com.adobe.coldfusion.http()
		};
		return this;
	}
	
	// PUBLIC
	
	public struct function read(
		required string source
	)
	{
		local.result = {};
		try
		{
			local.result = instance.cffeed.read( source=arguments.source );
		}
		catch( any exception ) 
		{
			// fallback method if cffeed fails because of itunes duration bug
			if( exception.msg IS "Invalid XML" )
				local.result = readFixingIntegerItunesDuration( arguments.source );
			else
				rethrow;
		}
		return local.result;
	}
	
	// PRIVATE
	
	private struct function readFixingIntegerItunesDuration(
		required string source
	)
	{
		// Use cfhttp instead of cffeed to get the feed xml string
		local.result = instance.cfhttp.send( url=arguments.source,timeout=10 ).getPrefix();
		local.fixedXml	= fixItunesDurationIntegers( local.result.fileContent );
		// store temporarily in memory
		local.tempFeedFilePath = "ram:///" & CreateUuid() & ".xml";
		FileWrite( local.tempFeedFilePath,ToString( local.fixedXml ) );
		// Now use the fixed xml file as the source for cffeed
		local.result = instance.cffeed.read( source=local.tempFeedFilePath );
		// clean up the temporary file
		FileDelete( local.tempFeedFilePath );
		return local.result;
	}
	
	private xml function fixItunesDurationIntegers(
		required string xmlString
	)
	{
		local.xmlObject = XmlParse( arguments.xmlString );
		local.items = XmlSearch( local.xmlObject,"/rss/channel/item" );
		for( var item in local.items )
		{
			local.duration = item[ "itunes:duration" ].xmlText;
			if( IsValid( "integer",local.duration ) )
			{
				// replace the integer seconds duration with HH:MM:SS format
				local.formattedDuration = timeFormattedDurationFromSeconds( local.duration );
				item[ "itunes:duration" ].xmlText = local.formattedDuration;
			}
		}
		return local.xmlObject;
	}
	
	// Adapted from http://www.cflib.org/udf/totalTimeFromSec by Hamlet Javier
	private string function timeFormattedDurationFromSeconds(
		required numeric totalSeconds
	)
	{
		local.hours = ( arguments.totalSeconds \ 3600 );
		local.minutes = ( arguments.totalSeconds \ 60 ) - ( local.hours * 60 );
		local.seconds = arguments.totalSeconds - ( local.hours * 3600 ) - ( local.minutes * 60 );
		return "#NumberFormat( local.hours,'00' )#:#NumberFormat( local.minutes,'00' )#:#NumberFormat( local.seconds,'00' )#";
	}

}

Use the component as follows:

request.reader = New podcastFeedReader();
request.result = request.reader.read( "[myPodcastFeedUrl]" );

Posted on . Updated

Comments

  • Formatting comments: See this list of formatting tags you can use in your comments.
  • Want to paste code? Enclose within <pre><code> tags for syntax higlighting and better formatting and if possible use script. If your code includes "self-closing" tags, such as <cfargument>, you must add an explicit closing tag, otherwise it is likely to be mangled by the Disqus parser.
Back to the top