Mercurial > ~darius > hgwebdir.cgi > amakode
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") |