Mercurial > ~darius > hgwebdir.cgi > amakode
view amakode.py @ 4:65a9f99302cd
Incorporate changes from Jens Zurheide <jens.zurheide@gmx.de> to read tags
from the source file and add them to the one being written.
Appears to work fine, however it should be optional. (ie work without tagpy
just not write tags)
author | darius@inchoate.localdomain |
---|---|
date | Mon, 12 Nov 2007 15:01:23 +1030 |
parents | de86a9e19151 |
children | f11c5ed0178e |
line wrap: on
line source
#!/usr/bin/env python ############################################################################ # Transcoder for Amarok # - Add support for tagging (jens.zurheide@gmx.de) # - Fixed typo in lame encoder (tcuya from kde-apps.org) # - Made setting maxjobs easier, although Amarok doesn't appear to issue # multiple requests :( # # Depends on: Python 2.2 # tagpy (optional) # # The only user servicable parts are the encode/decode (line 103) and the # number of concurrent jobs to run (line 225) # # The optional module tagpy (http://news.tiker.net/software/tagpy) is used # for tag information processing. This allows for writing tags into the # transcoded files. # ############################################################################ # # Copyright (C) 2007 Daniel O'Connor. All rights reserved. # Copyright (C) 2007 Jens Zurheide. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # ############################################################################ __version__ = "1.3" import ConfigParser import os import sys import string import signal import logging import select import subprocess import tempfile from logging.handlers import RotatingFileHandler import urllib import urlparse import re import tagpy class tagpywrap(dict): textfields = ['album', 'artist', 'title', 'comment', 'genre'] numfields = ['year', 'track'] allfields = textfields + numfields def __init__(self, url): f = urllib.urlopen(url) self.tagInfo = tagpy.FileRef(f.fp.name).tag() f.close() self['album'] = self.tagInfo.album.strip() self['artist'] = self.tagInfo.artist.strip() self['title'] = self.tagInfo.title.strip() self['comment'] = self.tagInfo.comment.strip() self['year'] = self.tagInfo.year self['genre'] = self.tagInfo.genre.strip() self['track'] = self.tagInfo.track for i in self.textfields: if (self[i] == ""): del self[i] for i in self.numfields: if (self[i] == 0): del self[i] class QueueMgr(object): queuedjobs = [] activejobs = [] def __init__(self, callback = None, maxjobs = 2): self.callback = callback self.maxjobs = maxjobs pass def add(self, job): log.debug("Job added") self.queuedjobs.append(job) def poll(self): """ Poll active jobs and check if we should make a new job active """ if (len(self.activejobs) == 0): needajob = True else: needajob = False for j in self.activejobs: if j.isfinished(): log.debug("job is done") needajob = True self.activejobs.remove(j) if (self.callback != None): self.callback(j) if needajob: #log.debug("Number of queued jobs = " + str(len(self.queuedjobs)) + ", number of active jobs = " + str(len(self.activejobs))) while len(self.queuedjobs) > 0 and len(self.activejobs) < self.maxjobs: newjob = self.queuedjobs.pop(0) newjob.start() self.activejobs.append(newjob) def isidle(self): """ Returns true if both queues are empty """ return(len(self.queuedjobs) == 0 and len(self.activejobs) == 0) class TranscodeJob(object): # Programs used to decode (to a wav stream) decode = {} decode["mp3"] = ["mpg123", "-w", "-", "-"] decode["ogg"] = ["ogg123", "-d", "wav", "-f", "-", "-"] # XXX: this is really fugly but faad refuses to read from a pipe decode["mp4"] = ["env", "MPLAYER_VERBOSE=-100", "mplayer", "-ao", "pcm:file=/dev/stdout", "-"] decode["m4a"] = decode["mp4"] decode["flac"] = ["flac", "-d", "-c", "-"] # Programs used to encode (from a wav stream) encode = {} encode["mp3"] = ["lame", "--abr", "128", "-", "-"] encode["ogg"] = ["oggenc", "-q", "2", "-"] encode["mp4"] = ["faac", "-wo", "/dev/stdout", "-"] encode["m4a"] = encode["mp4"] # XXX: can't encode flac - it's wav parser chokes on mpg123's output, it does work # OK if passed through sox but we can't do that. If you really want flac modify # the code & send me a diff or write a wrapper shell script :) #encode["flac"] = ["flac", "-c", "-"] # Options for output programs to store ID3 tag information tagopt = {} tagopt["mp3"] = { "album" : "--tl", "artist" : "--ta", "title" : "--tt", "track" : "--tn" } tagopt["ogg"] = { "album" : "-l", "artist" : "-a", "title" : "-a", "track" : "-N" } tagopt["mp4"] = { "album" : "--album", "artist" : "--artist", "title" : "--title", "track" : "--track" } #tagopt["flac"] = { "album" : "-Talbum=%s", "artist" : "-Tartist=%s", "title" : "-Ttitle=%s", "track" : "-Ttracknumber=%s" } def __init__(self, _inurl, _tofmt): self.errormsg = None log.debug("Creating job") self.inurl = _inurl self.tofmt = string.lower(_tofmt) self.inext = string.lower(string.rsplit(self.inurl, ".", 1)[1]) if (self.inext in self.decode): log.debug("can decode with " + str(self.decode[self.inext])) else: log.debug("unable to decode " + self.inext) raise KeyError("no available decoder") if (self.tofmt in self.encode): log.debug("can encode with " + str(self.encode[self.tofmt])) else: log.debug("unable to encode " + self.tofmt) raise KeyError("no available encoder") def start(self): log.debug("Starting job") try: self.inputfile = urllib.urlopen(self.inurl) self.outfd, self.outfname = tempfile.mkstemp(prefix="transcode-", suffix="." + self.tofmt) #self.outfname = string.join(string.rsplit(self.inurl, ".")[:-1] + [self.tofmt], ".") self.errfh, self.errfname = tempfile.mkstemp(prefix="transcode-", suffix=".log") self.outurl = urlparse.urlunsplit(["file", None, self.outfname, None, None]) log.debug("Outputting to " + self.outfname + " " + self.outurl + ")") log.debug("Errors to " + self.errfname) # assemble command line for encoder encoder = [] encoder += self.encode[self.tofmt] try: if (self.tofmt in self.tagopt): taginfo = tagpywrap(self.inurl) for f in taginfo.allfields: if (f in taginfo and f in self.tagopt[self.tofmt]): inf = taginfo[f] opt = self.tagopt[self.tofmt][f] log.debug(" %s = %s %s" % (f, opt, inf)) # If we have a substitution, make it. If # not append the info as a separate # arg. Note that the tag options are # passed in as the second option because a # lot of programs don't parse options # after their file list. if ('%s' in opt): opt = opt.replace('%s', inf) encoder.insert(1, opt) else: encoder.insert(1, opt) encoder.insert(2, inf) finally: pass log.debug("decoder -> " + str(self.decode[self.inext])) log.debug("encoder -> " + str(encoder)) self.decoder = subprocess.Popen(self.decode[self.inext], stdin=self.inputfile, stdout=subprocess.PIPE, stderr=self.errfh) self.encoder = subprocess.Popen(encoder, stdin=self.decoder.stdout, stdout=self.outfd, stderr=self.errfh) log.debug("Processes connected") except Exception, e: log.debug("Failed to start - " + str(e)) self.errormsg = str(e) try: os.unlink(self.outfname) except: pass def isfinished(self): if (self.errormsg != None): return(True) rtn = self.encoder.poll() if (rtn == None): return(False) if (rtn == 0): os.unlink(self.errfname) self.errormsg = None else: log.debug("error in transcode, please review " + self.errfname) self.errormsg = "Unable to transcode, please review " + self.errfname try: os.unlink(self.outfname) except: pass return(True) ############################################################################ # amaKode ############################################################################ class amaKode(object): """ The main application""" def __init__(self, args): """ Main loop waits for something to do then does it """ log.debug("Started.") self.readSettings() self.queue = QueueMgr(callback = self.notify, maxjobs = 1) while True: # Check for finished jobs, etc self.queue.poll() # Check if there's anything waiting on stdin res = select.select([sys.stdin.fileno()], [], [], 0.1) if (sys.stdin.fileno() in res[0]): # Let's hope we got a whole line or we stall here line = sys.stdin.readline() if line: self.customEvent(line) else: break def readSettings(self): """ Reads settings from configuration file """ try: foovar = config.get("General", "foo") except: log.debug("No config file found, using defaults.") def customEvent(self, string): """ Handles notifications """ #log.debug("Received notification: " + str(string)) if string.find("transcode") != -1: self.transcode(str(string)) if string.find("quit") != -1: self.quit() def transcode(self, line): """ Called when requested to transcode a track """ args = string.split(line) if (len(args) != 3): log.debug("Invalid transcode command") return log.debug("transcoding " + args[1] + " to " + args[2]) try: newjob = TranscodeJob(args[1], args[2]) except: log.debug("Can't create transcoding job") os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(args[1]) + "\"\"") self.queue.add(newjob) def notify(self, job): """ Report to amarok that the job is done """ if (job.errormsg == None): log.debug("Job " + job.inurl + " completed successfully") os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(job.inurl) + " " + re.escape(job.outurl)) else: log.debug("Job " + job.inurl + " failed - " + job.errormsg) os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(job.inurl) + "\"\"") def quit(self): log.debug("quitting") sys.exit() ############################################################################ def debug(message): """ Prints debug message to stdout """ log.debug(message) def onStop(signum, stackframe): """ Called when script is stopped by user """ log.debug("signalled exit") sys.exit() def initLog(): # Init our logging global log log = logging.getLogger("amaKode") # Default to warts and all logging log.setLevel(logging.DEBUG) # Log to this file logfile = logging.handlers.RotatingFileHandler(filename = "/tmp/amakode.log", maxBytes = 10000, backupCount = 3) # And stderr logstderr = logging.StreamHandler() # Format it nicely formatter = logging.Formatter("[%(name)s] %(message)s") # Glue it all together logfile.setFormatter(formatter) logstderr.setFormatter(formatter) log.addHandler(logfile) log.addHandler(logstderr) return(log) def reportJob(job): """ Report to amarok that the job is done """ if (job.errormsg == None): log.debug("Job " + job.inurl + " completed successfully") log.debug("dcop amarok mediabrowser transcodingFinished " + job.inurl + " " + job.outurl) else: log.debug("Job " + job.inurl + " failed - " + job.errormsg) log.debug("dcop amarok mediabrowser transcodingFinished " + job.inurl + "\"\"") if __name__ == "__main__": initLog() signal.signal(signal.SIGINT, onStop) signal.signal(signal.SIGHUP, onStop) signal.signal(signal.SIGTERM, onStop) if 1: # Run normal application app = amaKode(sys.argv) else: # Quick test case q = QueueMgr(reportJob) j = TranscodeJob("file:///tmp/test.mp3", "ogg") q.add(j) j2 = TranscodeJob("file:///tmp/test2.mp3", "m4a") q.add(j2) while not q.isidle(): q.poll() res = select.select([], [], [], 1) log.debug("jobs all done")