Media duration in Python on Mac OS X
We’re working on a podcast server and building the upload application using Django. Towards the bottom of my task list on this project is “Populate duration on upload for appropriate media”. That is, find a way to get the duration of each media file from within Python. It’s a step I’ve been dreading, because it looked like not only was it going to require some complicated libraries, it looked like it was going to require complicated libraries per media type: one for mp3, one for mpeg movies, etc.
The server we’re running this on is Mac OS X Leopard. We chose not to use Podcast Producer for it because we need anyone, regardless of platform, to be able to upload files. But I was wondering today if Podcast Producer’s workflows might be able to automatically optimize the uploaded media.
As I browsed through the Podcast Producer Workflow Tutorial, I discovered that the workflows are just Ruby scripts. And then I saw this snippet in a script that joined two media files:
- first_input_movie_time_range = OSX::QTMakeTimeRange(zero_time, first_input_movie.duration)
That looked a whole lot like a command line script to get the duration of a media file. I quickly used that as an example to whip up a Ruby script to get the duration of a media file.
[toggle code]
- #!/usr/bin/ruby
- require 'osx/cocoa'
- OSX.require_framework 'QTKit'
-
if ARGV.size != 1
- $stderr.puts "duration filename"
- exit(-1)
- end
- filename = ARGV[0]
- media, error = OSX::QTMovie.movieWithFile_error(filename)
-
if error != nil or media == nil:
- $stderr.puts "Could not load media file"
- exit(1)
- end
- duration = media.duration.timeValue/media.duration.timeScale
- $stderr.puts filename, media.duration.timeValue, media.duration.timeScale
- $stderr.puts "Duration: ", duration
Durations from QTMovie come with a time scale and a time value. The time value needs to be divided by the scale to provide the duration of that media file in seconds.
This Ruby script uses the Ruby/Cocoa bridge. But there’s also a Python/Cocoa bridge, PyObjC, included with Mac OS X. The next step was to build the same thing in Python:
[toggle code]
- #!/usr/bin/python
- from QTKit import QTMovie
- import sys
- from optparse import OptionParser
- parser = OptionParser()
- (options, args) = parser.parse_args()
-
if len(args) != 1:
- print "duration filename"
- sys.exit(0)
- filename = args[0]
- attributes = {'QTMovieFileNameAttribute': filename}
- movie, error = QTMovie.movieWithAttributes_error_(attributes, None)
-
if error or not movie:
- print "Problem with movie", filename, ":", error.description()
- sys.exit(0)
- duration = movie.duration().timeValue/movie.duration().timeScale
- print filename, "is", duration, "seconds long"
This script will provide the duration of mp3 files, QuickTime files, m4a files, mp4 files, and mpeg files, any file that QuickTime can open. The only file I ended up having trouble with was swf files, and for our purposes we can live without giving them a duration.
No Matching Architecture
So that is an example of how cool it is, sometimes, to work on the OS X command line: easy integration with multimedia from Python and Ruby scripts. I almost never give programming articles headline status on Mimsy, but this was cool enough that I considered it. It opens up an amazing amount of functionality to simple command line scripts. Some people get a tingle talking to politicians; I get a tingle typing “import WebKit”. Here, for example, is a command line script to convert the first page of a web site to a PDF file:
[toggle code]
- #!/usr/bin/python
- import WebKit, Foundation, AppKit
- from optparse import OptionParser
- import os.path
-
class appDelegate(Foundation.NSObject):
-
def applicationDidFinishLaunching_(self, notification):
- webview = notification.object().windows()[0].contentView()
- webview.frameLoadDelegate().loadURL(webview)
-
def applicationDidFinishLaunching_(self, notification):
-
class webPrinter(Foundation.NSObject, WebKit.protocols.WebFrameLoadDelegate):
-
def loadURL(self, webview):
- page = Foundation.NSURL.URLWithString_(url)
- pageRequest = Foundation.NSURLRequest.requestWithURL_(page)
- webview.mainFrame().loadRequest_(pageRequest)
-
def webView_didFinishLoadForFrame_(self, webView, frame):
- printAttributes = AppKit.NSPrintInfo.sharedPrintInfo().dictionary()
- printAttributes[AppKit.NSPrintSavePath] = filePath
- printAttributes[AppKit.NSPrintJobDisposition] = AppKit.NSPrintSaveJob
- printInfo = AppKit.NSPrintInfo.alloc().initWithDictionary_(printAttributes)
- #scale the page horizontally to fit on one page, width-wise
- printInfo.setHorizontalPagination_(AppKit.NSFitPagination)
- preferences = webView.preferences()
-
if (options.screen):
- preferences.setShouldPrintBackgrounds_(True)
- webView.setMediaStyle_("screen")
-
else:
- #shouldPrintBackgrounds seems to stick
- preferences.setShouldPrintBackgrounds_(False)
- #pdf = frame.frameView().printOperationWithPrintInfo_(printInfo)
- pdf = AppKit.NSPrintOperation.printOperationWithView_printInfo_(webView, printInfo)
- pdf.setJobTitle_(webView.mainFrameTitle())
- pdf.setShowsPrintPanel_(False)
- pdf.setShowsProgressPanel_(False)
- pdf.runOperation()
- AppKit.NSApplication.sharedApplication().terminate_(None)
-
def loadURL(self, webview):
- parser = OptionParser()
- parser.add_option("-s", "--screen", help="print as it appears on the screen", action="store_true", dest="screen")
- (options, args) = parser.parse_args()
-
if len(args) != 2:
- print "Usage: webToPDF URL filename"
-
else:
- url = args[0]
- filePath = args[1]
- #NSPrintOperation requires a full path or it will ignore NSPrintSaveJob and print instead
- filePath = os.path.abspath(filePath)
- #this looks a lot like inches times 100
- viewRect = Foundation.NSMakeRect(0, 0, 850, 1100)
- app = AppKit.NSApplication.sharedApplication()
- delegate = appDelegate.alloc().init()
- AppKit.NSApp().setDelegate_(delegate)
- window = AppKit.NSWindow.alloc()
- window.initWithContentRect_styleMask_backing_defer_(viewRect, 0, AppKit.NSBackingStoreBuffered, False)
- view = WebKit.WebView.alloc()
- view.initWithFrame_(viewRect)
- view.mainFrame().frameView().setAllowsScrolling_(False)
- window.setContentView_(view)
- loadDelegate = webPrinter.alloc().init()
- view.setFrameLoadDelegate_(loadDelegate)
- app.run()
Unfortunately, the coolness is mitigated by the inability to use either of these scripts in a server context. In the above example, it won’t run under most servers, because the windowing portions of it requires an active login. And the duration script runs into problems on Intel servers because Apache is 64-bit but PyObjC is not. OS X won’t load PyObjC into Apache because of that mismatch. If I try to “import QTKit” into our Django application, I get:
Could not import files.views. Error was: dlopen(/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC/objc/_objc.so, 2): no suitable image found. Did find: /System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC/objc/_objc.so: no matching architecture in universal wrapper
Fear of that error was the main reason I dreaded having to install a bunch of libraries in order to get media file durations. There’s pretty much no way around this without seriously hacking either the default PyObjC installation or the default Apache installation. Fortunately, for this purpose I only need to get the duration when someone uploads a large media file. Another second or two from shelling out after an upload won’t matter.
[toggle code]
- import datetime, subprocess
- …
-
class Upload(models.Model):
- file = models.FileField(upload_to=fileUploadPath, validator_list=[fileValidator])
-
…
-
def save(self):
- super(Upload, self).save()
-
if self.duration == None:
-
if self.filetype.extension in ['mp3', 'mov', 'm4a', 'mp4', 'mpg']:
- cmd = "/Users/holden/bin/duration"
- pipe = subprocess.Popen([cmd, self.file.path], stdout=subprocess.PIPE)
- duration = pipe.stdout.read()
- pipe.stdout.close
- duration = int(duration)
-
if duration:
- duration = datetime.timedelta(seconds=duration)
- self.duration = str(duration)
- self.save()
-
if self.filetype.extension in ['mp3', 'mov', 'm4a', 'mp4', 'mpg']:
-
def save(self):
In the “duration” script, change the “print” line to simply “print duration”, so that it only outputs the number of seconds. Also, in the “fileUploadPath” function that I give upload_to, I have it set instance.duration to None so that I only grab the duration when a new file has been uploaded.
The next version of PyObjC is 64-bit compatible, so if we’re lucky shelling out won’t be necessary in a future update of Leopard server.
- Podcast Producer Workflow Tutorial
- “Podcast Producer Workflow Tutorial takes you step by step through the process of developing a workflow incrementally.”
- Creating iPhone-compatible movies with PyObjC and QTKit
- “As part of some general messing around with PyObjC and QTKit, I wrote a short script for converting a movie (anything readable by QuickTime) into an iPhone-compatible format.”
- PyObjC
- “The PyObjC project aims to provide a bridge between the Python and Objective-C programming languages. The bridge is intended to be fully bidirectional, allowing the Python programmer to take full advantage of the power provided by various Objective-C based toolkits and the Objective-C programmer transparent access to Python based functionality.”
- webkit2png
- “webkit2png is a command line tool that creates png screenshots of webpages.”
- WebKit: Simple Browsing
- “By writing just a few lines of code using the Web Kit, you can embed web content in your application and enable your users to navigate the web.”
- Cocoa Dev Central: Create a PDF
- “In this tutorial we'll look at how to add PDF exportation to an application. It is an easy to add feature that can add a lot of functionality to your program. We'll start off by briefly looking at the commonly shown export method and then implement a method that will export multi-page PDFs.”
- WebView Class Reference
- “WebView is the core view class in the Web Kit Framework that manages interactions between WebFrame and WebFrameView classes. To embed web content in your application, you just create a WebView object, attach it to a window, and send a loadRequest: message to its main frame.”
- NSPrintOperation Class Reference
- “An NSPrintOperation object controls operations that generate Encapsulated PostScript (EPS) code, Portable Document Format (PDF) code, or print jobs. An NSPrintOperation object works in conjunction with two other objects: an NSPrintInfo object, which specifies how the code should be generated, and an NSView object, which generates the actual code.”
More Python
- Multiple Input Fields with multiple inheritance
- We needed to display one TextField as either a TextInput or a Textarea, depending on the value in the field. Multiple inheritance makes it easy, if a bit wonky.
- PyTown
- General rambling in code regarding Python, Mailman, and Django.
- Thinking Python: Django cache expiration time
- Django sets the expiration time when data is cached. Sometimes it makes more sense to expire data dynamically based on later changes to the database. Does this mean a change to CacheClass? Not necessarily.
- Django Twitter tag and RSS object
- I wanted to embed my twitter feed into my Django blog, and didn’t see any simple RSS readers for Python that did what I wanted.
- Excerpting partial XHTML using minidom
- You can use xml.dom.minidom to parse partial XHTML as long as you use a few tricks and don’t mind that getElementById doesn’t work.
- 18 more pages with the topic Python, and other related pages
More Mac OS X tricks
- Stack windows on top of each other
- If you want to stack multiple windows directly on top of each other, it’s easy to do in any well-behaved application, such as Nisus Writer Pro, Safari, Mail, and even older applications like AppleWorks 6 and Microsoft Word X.
- Leopard setuid and passwd file changes
- Leopard Server introduced two major changes to two lesser-used features: setuid root script wrappers and BSD flat file authentication.
- SilverService and Taskpaper
- SilverService is a great little app if you commonly need to repetitiously modify text. Any application that supports services will support running selected text through command-line scripts via SilverService.
- Combining multiple PDF files into a single file
- Automator allows you to combine multiple PDF files into a single file.
- Using Exposé effectively on Mac OS X
- A few simple tricks with window management in OS X can make working with multiple applications or multiple windows much easier and faster.
- Six more pages with the topic Mac OS X tricks, and other related pages
