Home > Techniques > Pulling metadata out of enhanced podcasts (.m4a) and into XML

Pulling metadata out of enhanced podcasts (.m4a) and into XML

puzzle_pieces300x199

For my radio show iPhone application project, I wanted to use the metadata contained in my enhanced podcast files. Including:

  • chapter information (artist/song)
  • chapter start time
  • chapter image

Once I had this information I wanted to store it in XML on my server so I could access it whenever I wanted (from the iPhone application).


I accomplished this by writing a simple little command line utility that parses an enhanced podcast .m4a file with QTKit and pulls out the metadata, and adds it to an XML file. Once you track down all the details, it is fairly simple, but I figured maybe it would help people to see all the information in one place. Obviously my code is tailored for my radio show example, but I think it is fairly clear how to translate it for other uses.

Here is the most interesting function of my project:

void addFileToTree( NSXMLElement* inRoot, NSString* inFileName )
{
	QTMovie* movie = [QTMovie movieWithFile:inFileName error:nil];

	NSInteger numChapters = [movie chapterCount];
	NSLog(@"Number of Chapters: %d", numChapters);

	NSXMLElement *showNode = [NSXMLNode elementWithName:@"Show"];
	[inRoot addChild:showNode];

	NSString* baseShowName = [inFileName stringByReplacingOccurrencesOfString:@".m4a" withString:@""];

	NSString* mp3Filename = [inFileName stringByReplacingOccurrencesOfString:@".m4a" withString:@".mp3"];
	[showNode addChild:[NSXMLNode elementWithName:@"Filename" stringValue:mp3Filename  ] ];

	NSDictionary* movieAttrs = [movie movieAttributes];
	NSString* movieDisplayName = [movieAttrs objectForKey:@"QTMovieDisplayNameAttribute"];
	[showNode addChild:[NSXMLNode elementWithName:@"EpisodeName" stringValue:movieDisplayName  ] ];

	NSDate* movieCreationDate = [movieAttrs objectForKey:@"QTMovieCreationTimeAttribute"];
	[showNode addChild:[NSXMLNode elementWithName:@"Date" stringValue:[movieCreationDate description]  ] ];

	NSArray* chapterArray = [movie chapters];
	NSInteger curChap = 0;
	for ( NSDictionary* chapDict in chapterArray )
	{
		curChap++;

		NSXMLElement *songNode = [NSXMLNode elementWithName:@"Song"];
		[showNode addChild:songNode];

		NSValue* startTimeValue = [chapDict objectForKey:@"QTMovieChapterStartTime"];

		QTTime startTime;
		[startTimeValue getValue:&startTime];

		NSInteger offsetInSeconds = startTime.timeValue / startTime.timeScale;

		/////////////////////
		NSArray* trackArray = [movie tracksOfMediaType:QTMediaTypeText];

		for (int i=0; i < [trackArray count]; i++)
		{
			QTTrack* thisTrack = [trackArray objectAtIndex:i];
			[thisTrack setEnabled:NO];
		}

		NSDictionary *imageAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
									QTMovieFrameImageTypeNSImage, QTMovieFrameImageType,
									[NSNumber numberWithBool:YES], QTMovieFrameImageHighQuality,
									[NSValue valueWithSize:NSMakeSize(320, 320)], QTMovieFrameImageSize, nil];

		NSImage* chapImage = [movie frameImageAtTime:startTime withAttributes:imageAttrs error:nil];

 		// write image to disk
		NSString* filenameString = [NSString stringWithFormat:@"%@_%d.jpg", baseShowName, curChap];

		NSArray *representations;
		NSData *bitmapData;

		representations = [chapImage representations];

		// Specify that we want to save the file as a JPG
		bitmapData = [NSBitmapImageRep representationOfImageRepsInArray:representations
															  usingType:NSJPEGFileType
															 properties:[NSDictionary dictionaryWithObject:[NSDecimalNumber numberWithFloat:0.6]
																									forKey:NSImageCompressionFactor]];

		[bitmapData writeToFile:filenameString  atomically:YES];

		/////////////////////

		[songNode addChild:[NSXMLNode elementWithName:@"Chapter" stringValue:[NSString stringWithFormat:@"%d", curChap] ] ];

		NSString* chapterName = [chapDict objectForKey:@"QTMovieChapterName"];
		NSArray *songArray = [chapterName componentsSeparatedByString:@" - "];

		if ( [songArray count] == 1 )
		{
			[songNode addChild:[NSXMLNode elementWithName:@"Artist" stringValue:@"PJ Gray"  ] ];
			[songNode addChild:[NSXMLNode elementWithName:@"Title" stringValue:[songArray objectAtIndex:0]  ] ];
		}
		else
		{
			[songNode addChild:[NSXMLNode elementWithName:@"Artist" stringValue:[songArray objectAtIndex:0]  ] ];
			[songNode addChild:[NSXMLNode elementWithName:@"Title" stringValue: [songArray objectAtIndex:1] ] ];
		}
		[songNode addChild:[NSXMLNode elementWithName:@"StartOffset" stringValue: [NSString stringWithFormat:@"%d", offsetInSeconds] ] ];

		NSValue* endTimeValue;
		if ( curChap <= [chapterArray count]-1 )
		{
			NSDictionary* nextChap = [chapterArray objectAtIndex:curChap];
			endTimeValue = [nextChap objectForKey:@"QTMovieChapterStartTime"];
		}
		else
			endTimeValue = [movieAttrs objectForKey:@"QTMovieDurationAttribute"];

		QTTime endTime;
		[endTimeValue getValue:&endTime];
		NSInteger endtimeInSeconds = endTime.timeValue / endTime.timeScale;
		[songNode addChild:[NSXMLNode elementWithName:@"EndOffset" stringValue:[NSString stringWithFormat:@"%d", endtimeInSeconds] ] ];

		[songNode addChild:[NSXMLNode elementWithName:@"Image" stringValue:filenameString ] ];

		NSLog(@"%@",  [chapDict objectForKey:@"QTMovieChapterName"]);
	}

}

That is a huge function, I know. However, it really does simple tasks. It is using QTKit to take the metadata out of the m4a file, including both offset times, and image information. The most interesting bit is probably the exact code to write the image information out as a jpg, as that take a bit more than what the docs describe. If you have any questions or comments, please let me know!

Download source: m4a2xml

  1. October 2nd, 2009 at 13:01 | #1

    This is really interesting. Thanks for sharing! I’m curious, do you know of any web-embeddable audio player that displays the metadata in m4a files such as chapters? Thanks. :)

  2. pj4533
    October 2nd, 2009 at 13:18 | #2

    Hmmm, not really. Back when I was working on this, the only thing I found was QTKit, and as you can see I wrote that as a command line app. Best bet is probably to pull the information out as I do here, and store as an xml file in a web accessible location. Then you could make a webapp to do anything you wanted with that data.

    But, that is all well and good….just beware, 99% of anything you would want to do with this kind of technology violates the DMCA. As I unfortunately discovered…. :(

  1. No trackbacks yet.