comparison 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
comparison
equal deleted inserted replaced
3:de86a9e19151 4:65a9f99302cd
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 ############################################################################ 3 ############################################################################
4 # Transcoder for Amarok 4 # Transcoder for Amarok
5 # (c) 2007 Daniel O'Connor <darius@dons.net.au> 5 # - Add support for tagging (jens.zurheide@gmx.de)
6 # - Fixed typo in lame encoder (tcuya from kde-apps.org)
7 # - Made setting maxjobs easier, although Amarok doesn't appear to issue
8 # multiple requests :(
6 # 9 #
7 # Depends on: Python 2.2 10 # Depends on: Python 2.2
11 # tagpy (optional)
12 #
13 # The only user servicable parts are the encode/decode (line 103) and the
14 # number of concurrent jobs to run (line 225)
15 #
16 # The optional module tagpy (http://news.tiker.net/software/tagpy) is used
17 # for tag information processing. This allows for writing tags into the
18 # transcoded files.
8 # 19 #
9 ############################################################################ 20 ############################################################################
10 # 21 #
11 # Copyright (C) 2007 Daniel O'Connor. All rights reserved. 22 # Copyright (C) 2007 Daniel O'Connor. All rights reserved.
23 # Copyright (C) 2007 Jens Zurheide. All rights reserved.
12 # 24 #
13 # Redistribution and use in source and binary forms, with or without 25 # Redistribution and use in source and binary forms, with or without
14 # modification, are permitted provided that the following conditions 26 # modification, are permitted provided that the following conditions
15 # are met: 27 # are met:
16 # 1. Redistributions of source code must retain the above copyright 28 # 1. Redistributions of source code must retain the above copyright
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 42 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 43 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 # SUCH DAMAGE. 44 # SUCH DAMAGE.
33 # 45 #
34 ############################################################################ 46 ############################################################################
47
48 __version__ = "1.3"
35 49
36 import ConfigParser 50 import ConfigParser
37 import os 51 import os
38 import sys 52 import sys
39 import string 53 import string
44 import tempfile 58 import tempfile
45 from logging.handlers import RotatingFileHandler 59 from logging.handlers import RotatingFileHandler
46 import urllib 60 import urllib
47 import urlparse 61 import urlparse
48 import re 62 import re
49 63 import tagpy
50 class QueueMgr: 64
65 class tagpywrap(dict):
66 textfields = ['album', 'artist', 'title', 'comment', 'genre']
67 numfields = ['year', 'track']
68 allfields = textfields + numfields
69
70 def __init__(self, url):
71 f = urllib.urlopen(url)
72
73 self.tagInfo = tagpy.FileRef(f.fp.name).tag()
74 f.close()
75
76 self['album'] = self.tagInfo.album.strip()
77 self['artist'] = self.tagInfo.artist.strip()
78 self['title'] = self.tagInfo.title.strip()
79 self['comment'] = self.tagInfo.comment.strip()
80 self['year'] = self.tagInfo.year
81 self['genre'] = self.tagInfo.genre.strip()
82 self['track'] = self.tagInfo.track
83 for i in self.textfields:
84 if (self[i] == ""):
85 del self[i]
86
87 for i in self.numfields:
88 if (self[i] == 0):
89 del self[i]
90
91 class QueueMgr(object):
51 queuedjobs = [] 92 queuedjobs = []
52 activejobs = [] 93 activejobs = []
53 94
54 def __init__(self, callback = None, maxjobs = 2): 95 def __init__(self, callback = None, maxjobs = 2):
55 self.callback = callback 96 self.callback = callback
84 125
85 def isidle(self): 126 def isidle(self):
86 """ Returns true if both queues are empty """ 127 """ Returns true if both queues are empty """
87 return(len(self.queuedjobs) == 0 and len(self.activejobs) == 0) 128 return(len(self.queuedjobs) == 0 and len(self.activejobs) == 0)
88 129
89 class TranscodeJob: 130 class TranscodeJob(object):
90 # Programs used to decode (to a wav stream) 131 # Programs used to decode (to a wav stream)
91 decode = {} 132 decode = {}
92 decode["mp3"] = ["mpg123", "-w", "-", "-"] 133 decode["mp3"] = ["mpg123", "-w", "-", "-"]
93 decode["ogg"] = ["ogg123", "-d", "wav", "-f", "-", "-"] 134 decode["ogg"] = ["ogg123", "-d", "wav", "-f", "-", "-"]
94 # XXX: this is really fugly but faad refuses to read from a pipe 135 # XXX: this is really fugly but faad refuses to read from a pipe
98 139
99 # Programs used to encode (from a wav stream) 140 # Programs used to encode (from a wav stream)
100 encode = {} 141 encode = {}
101 encode["mp3"] = ["lame", "--abr", "128", "-", "-"] 142 encode["mp3"] = ["lame", "--abr", "128", "-", "-"]
102 encode["ogg"] = ["oggenc", "-q", "2", "-"] 143 encode["ogg"] = ["oggenc", "-q", "2", "-"]
103 encode["mp4"] = ["faac", "-o", "/dev/stdout", "-"] 144 encode["mp4"] = ["faac", "-wo", "/dev/stdout", "-"]
104 encode["m4a"] = encode["mp4"] 145 encode["m4a"] = encode["mp4"]
105 encode["flac"] = ["flac", "-c", "-"] 146
147 # XXX: can't encode flac - it's wav parser chokes on mpg123's output, it does work
148 # OK if passed through sox but we can't do that. If you really want flac modify
149 # the code & send me a diff or write a wrapper shell script :)
150 #encode["flac"] = ["flac", "-c", "-"]
151
152 # Options for output programs to store ID3 tag information
153 tagopt = {}
154 tagopt["mp3"] = { "album" : "--tl", "artist" : "--ta", "title" : "--tt", "track" : "--tn" }
155 tagopt["ogg"] = { "album" : "-l", "artist" : "-a", "title" : "-a", "track" : "-N" }
156 tagopt["mp4"] = { "album" : "--album", "artist" : "--artist", "title" : "--title", "track" : "--track" }
157 #tagopt["flac"] = { "album" : "-Talbum=%s", "artist" : "-Tartist=%s", "title" : "-Ttitle=%s", "track" : "-Ttracknumber=%s" }
106 158
107 def __init__(self, _inurl, _tofmt): 159 def __init__(self, _inurl, _tofmt):
108 self.errormsg = None 160 self.errormsg = None
109 log.debug("Creating job") 161 log.debug("Creating job")
110 self.inurl = _inurl 162 self.inurl = _inurl
131 183
132 self.errfh, self.errfname = tempfile.mkstemp(prefix="transcode-", suffix=".log") 184 self.errfh, self.errfname = tempfile.mkstemp(prefix="transcode-", suffix=".log")
133 self.outurl = urlparse.urlunsplit(["file", None, self.outfname, None, None]) 185 self.outurl = urlparse.urlunsplit(["file", None, self.outfname, None, None])
134 log.debug("Outputting to " + self.outfname + " " + self.outurl + ")") 186 log.debug("Outputting to " + self.outfname + " " + self.outurl + ")")
135 log.debug("Errors to " + self.errfname) 187 log.debug("Errors to " + self.errfname)
136 self.decoder = subprocess.Popen(self.decode[self.inext], stdin=self.inputfile, stdout=subprocess.PIPE, stderr=self.errfd) 188
137 self.encoder = subprocess.Popen(self.encode[self.tofmt], stdin=self.decoder.stdout, stdout=self.outfd, stderr=self.errfd) 189 # assemble command line for encoder
190 encoder = []
191 encoder += self.encode[self.tofmt]
192
193 try:
194 if (self.tofmt in self.tagopt):
195 taginfo = tagpywrap(self.inurl)
196 for f in taginfo.allfields:
197 if (f in taginfo and f in self.tagopt[self.tofmt]):
198 inf = taginfo[f]
199 opt = self.tagopt[self.tofmt][f]
200 log.debug(" %s = %s %s" % (f, opt, inf))
201 # If we have a substitution, make it. If
202 # not append the info as a separate
203 # arg. Note that the tag options are
204 # passed in as the second option because a
205 # lot of programs don't parse options
206 # after their file list.
207 if ('%s' in opt):
208 opt = opt.replace('%s', inf)
209 encoder.insert(1, opt)
210 else:
211 encoder.insert(1, opt)
212 encoder.insert(2, inf)
213 finally:
214 pass
215
216 log.debug("decoder -> " + str(self.decode[self.inext]))
217 log.debug("encoder -> " + str(encoder))
218 self.decoder = subprocess.Popen(self.decode[self.inext], stdin=self.inputfile, stdout=subprocess.PIPE, stderr=self.errfh)
219 self.encoder = subprocess.Popen(encoder, stdin=self.decoder.stdout, stdout=self.outfd, stderr=self.errfh)
220 log.debug("Processes connected")
138 except Exception, e: 221 except Exception, e:
139 log.debug("Failed to start - " + str(e)) 222 log.debug("Failed to start - " + str(e))
140 self.errormsg = str(e) 223 self.errormsg = str(e)
141 try: 224 try:
142 os.unlink(self.outfname) 225 os.unlink(self.outfname)
165 return(True) 248 return(True)
166 249
167 ############################################################################ 250 ############################################################################
168 # amaKode 251 # amaKode
169 ############################################################################ 252 ############################################################################
170 class amaKode: 253 class amaKode(object):
171 """ The main application""" 254 """ The main application"""
172 255
173 def __init__(self, args): 256 def __init__(self, args):
174 """ Main loop waits for something to do then does it """ 257 """ Main loop waits for something to do then does it """
175 log.debug("Started.") 258 log.debug("Started.")
288 initLog() 371 initLog()
289 signal.signal(signal.SIGINT, onStop) 372 signal.signal(signal.SIGINT, onStop)
290 signal.signal(signal.SIGHUP, onStop) 373 signal.signal(signal.SIGHUP, onStop)
291 signal.signal(signal.SIGTERM, onStop) 374 signal.signal(signal.SIGTERM, onStop)
292 if 1: 375 if 1:
376 # Run normal application
293 app = amaKode(sys.argv) 377 app = amaKode(sys.argv)
294 else: 378 else:
295 # Quick test case 379 # Quick test case
296 q = QueueMgr(reportJob) 380 q = QueueMgr(reportJob)
297 j = TranscodeJob("file:///tmp/test.mp3", "ogg") 381 j = TranscodeJob("file:///tmp/test.mp3", "ogg")