Mercurial > ~darius > hgwebdir.cgi > amakode
comparison amakode.py @ 1:07e3d8655a29 AMAKODE_1_0
Initial import of Amakode a transcoding script for Amarok written entirely
in Python.
author | darius |
---|---|
date | Sat, 31 Mar 2007 02:09:51 +0000 |
parents | |
children | de86a9e19151 |
comparison
equal
deleted
inserted
replaced
0:a976c9c2cc20 | 1:07e3d8655a29 |
---|---|
1 #!/usr/bin/env python | |
2 | |
3 ############################################################################ | |
4 # Transcoder for Amarok | |
5 # (c) 2007 Daniel O'Connor <darius@dons.net.au> | |
6 # | |
7 # Depends on: Python 2.2 | |
8 # | |
9 ############################################################################ | |
10 # | |
11 # Copyright (C) 2007 Daniel O'Connor. All rights reserved. | |
12 # | |
13 # Redistribution and use in source and binary forms, with or without | |
14 # modification, are permitted provided that the following conditions | |
15 # are met: | |
16 # 1. Redistributions of source code must retain the above copyright | |
17 # notice, this list of conditions and the following disclaimer. | |
18 # 2. Redistributions in binary form must reproduce the above copyright | |
19 # notice, this list of conditions and the following disclaimer in the | |
20 # documentation and/or other materials provided with the distribution. | |
21 # | |
22 # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |
23 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
24 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
25 # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE | |
26 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
27 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
28 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
29 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
30 # 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 | |
32 # SUCH DAMAGE. | |
33 # | |
34 ############################################################################ | |
35 | |
36 import ConfigParser | |
37 import os | |
38 import sys | |
39 import string | |
40 import signal | |
41 import logging | |
42 import select | |
43 import subprocess | |
44 import tempfile | |
45 from logging.handlers import RotatingFileHandler | |
46 import urllib | |
47 import urlparse | |
48 import re | |
49 | |
50 class QueueMgr: | |
51 queuedjobs = [] | |
52 activejobs = [] | |
53 maxjobs = 1 | |
54 | |
55 def __init__(self, _callback): | |
56 self.callback = _callback | |
57 pass | |
58 | |
59 def add(self, job): | |
60 log.debug("Job added") | |
61 self.queuedjobs.append(job) | |
62 | |
63 def poll(self): | |
64 """ Poll active jobs and check if we should make a new job active """ | |
65 if (len(self.activejobs) == 0): | |
66 needajob = True | |
67 else: | |
68 needajob = False | |
69 | |
70 for j in self.activejobs: | |
71 if j.isfinished(): | |
72 log.debug("job is done") | |
73 needajob = True | |
74 self.activejobs.remove(j) | |
75 if (self.callback != None): | |
76 self.callback(j) | |
77 | |
78 if needajob: | |
79 #log.debug("Number of queued jobs = " + str(len(self.queuedjobs)) + ", number of active jobs = " + str(len(self.activejobs))) | |
80 while len(self.queuedjobs) > 0 and len(self.activejobs) < self.maxjobs: | |
81 newjob = self.queuedjobs.pop(0) | |
82 newjob.start() | |
83 self.activejobs.append(newjob) | |
84 | |
85 def isidle(self): | |
86 """ Returns true if both queues are empty """ | |
87 return(len(self.queuedjobs) == 0 and len(self.activejobs) == 0) | |
88 | |
89 class TranscodeJob: | |
90 # Programs used to decode (to a wav stream) | |
91 decode = {} | |
92 decode["mp3"] = ["mpg123", "-w", "-", "-"] | |
93 decode["ogg"] = ["ogg123", "-d", "wav", "-f", "-", "-"] | |
94 # XXX: this is really fugly but faad refuses to read from a pipe | |
95 decode["mp4"] = ["env", "MPLAYER_VERBOSE=-100", "mplayer", "-ao", "pcm:file=/dev/stdout", "-"] | |
96 decode["m4a"] = decode["mp4"] | |
97 decode["flac"] = ["flac", "-d", "-c", "-"] | |
98 | |
99 # Programs used to encode (from a wav stream) | |
100 encode = {} | |
101 encode["mp3"] = ["lame", "--abr", "128", "-", "-"] | |
102 encode["ogg"] = ["oggenc", "-q", "2", "-"] | |
103 encode["mp4"] = ["faac", "-o", "/dev/stdout", "-"] | |
104 encode["m4a"] = encode["mp4"] | |
105 encode["flac"] = ["flac", "-c", "-"] | |
106 | |
107 def __init__(self, _inurl, _tofmt): | |
108 self.outfname = None | |
109 self.errfname = None | |
110 self.errormsg = None | |
111 log.debug("Creating job") | |
112 self.inurl = _inurl | |
113 self.tofmt = string.lower(_tofmt) | |
114 self.inext = string.lower(string.rsplit(self.inurl, ".", 1)[1]) | |
115 if (self.inext in self.decode): | |
116 log.debug("can decode with " + str(self.decode[self.inext])) | |
117 else: | |
118 log.debug("unable to decode " + self.inext) | |
119 raise KeyError("no available decoder") | |
120 | |
121 if (self.tofmt in self.encode): | |
122 log.debug("can encode with " + str(self.encode[self.tofmt])) | |
123 else: | |
124 log.debug("unable to encode " + self.tofmt) | |
125 raise KeyError("no available encoder") | |
126 | |
127 def start(self): | |
128 log.debug("Starting job") | |
129 try: | |
130 self.inputfile = urllib.urlopen(self.inurl) | |
131 self.outfd, self.outfname = tempfile.mkstemp(prefix="transcode-", suffix="." + self.tofmt) | |
132 #self.outfname = string.join(string.rsplit(self.inurl, ".")[:-1] + [self.tofmt], ".") | |
133 | |
134 self.errfd, self.errfname = tempfile.mkstemp(prefix="transcode-", suffix=".log") | |
135 self.outurl = urlparse.urlunsplit(["file", None, self.outfname, None, None]) | |
136 log.debug("Outputting to " + self.outfname + " " + self.outurl + ")") | |
137 log.debug("Errors to " + self.errfname) | |
138 self.decoder = subprocess.Popen(self.decode[self.inext], stdin=self.inputfile, stdout=subprocess.PIPE, stderr=self.errfd) | |
139 self.encoder = subprocess.Popen(self.encode[self.tofmt], stdin=self.decoder.stdout, stdout=self.outfd, stderr=self.errfd) | |
140 except Exception, e: | |
141 log.debug("Failed to start - " + str(e)) | |
142 self.errormsg = str(e) | |
143 try: | |
144 os.unlink(self.outfname) | |
145 except: | |
146 pass | |
147 | |
148 def isfinished(self): | |
149 if (self.errormsg != None): | |
150 return(True) | |
151 | |
152 rtn = self.encoder.poll() | |
153 if (rtn == None): | |
154 return(False) | |
155 | |
156 os.close(self.errfd) | |
157 os.close(self.outfd) | |
158 | |
159 if (rtn == 0): | |
160 os.unlink(self.errfname) | |
161 self.errormsg = None | |
162 else: | |
163 log.debug("error in transcode, please review " + self.errfname) | |
164 self.errormsg = "Unable to transcode, please review " + self.errfname | |
165 try: | |
166 os.unlink(self.outfname) | |
167 except: | |
168 pass | |
169 | |
170 return(True) | |
171 | |
172 ############################################################################ | |
173 # amaKode | |
174 ############################################################################ | |
175 class amaKode: | |
176 """ The main application""" | |
177 | |
178 def __init__(self, args): | |
179 """ Main loop waits for something to do then does it """ | |
180 log.debug("Started.") | |
181 | |
182 self.readSettings() | |
183 | |
184 self.queue = QueueMgr(self.notify) | |
185 | |
186 while True: | |
187 # Check for finished jobs, etc | |
188 self.queue.poll() | |
189 # Check if there's anything waiting on stdin | |
190 res = select.select([sys.stdin.fileno()], [], [], 0.1) | |
191 if (sys.stdin.fileno() in res[0]): | |
192 # Let's hope we got a whole line or we stall here | |
193 line = sys.stdin.readline() | |
194 if line: | |
195 self.customEvent(line) | |
196 else: | |
197 break | |
198 | |
199 def readSettings(self): | |
200 """ Reads settings from configuration file """ | |
201 | |
202 try: | |
203 foovar = config.get("General", "foo") | |
204 | |
205 except: | |
206 log.debug("No config file found, using defaults.") | |
207 | |
208 def customEvent(self, string): | |
209 """ Handles notifications """ | |
210 | |
211 #log.debug("Received notification: " + str(string)) | |
212 | |
213 if string.find("transcode") != -1: | |
214 self.transcode(str(string)) | |
215 | |
216 if string.find("quit") != -1: | |
217 self.quit() | |
218 | |
219 def transcode(self, line): | |
220 """ Called when requested to transcode a track """ | |
221 args = string.split(line) | |
222 if (len(args) != 3): | |
223 log.debug("Invalid transcode command") | |
224 return | |
225 | |
226 log.debug("transcoding " + args[1] + " to " + args[2]) | |
227 try: | |
228 newjob = TranscodeJob(args[1], args[2]) | |
229 except: | |
230 log.debug("Can't create transcoding job") | |
231 os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(args[1]) + "\"\"") | |
232 | |
233 self.queue.add(newjob) | |
234 | |
235 def notify(self, job): | |
236 """ Report to amarok that the job is done """ | |
237 if (job.errormsg == None): | |
238 log.debug("Job " + job.inurl + " completed successfully") | |
239 os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(job.inurl) + " " + re.escape(job.outurl)) | |
240 else: | |
241 log.debug("Job " + job.inurl + " failed - " + job.errormsg) | |
242 os.system("dcop amarok mediabrowser transcodingFinished " + re.escape(job.inurl) + "\"\"") | |
243 | |
244 def quit(self): | |
245 log.debug("quitting") | |
246 sys.exit() | |
247 | |
248 ############################################################################ | |
249 | |
250 def debug(message): | |
251 """ Prints debug message to stdout """ | |
252 log.debug(message) | |
253 | |
254 def onStop(signum, stackframe): | |
255 """ Called when script is stopped by user """ | |
256 log.debug("signalled exit") | |
257 sys.exit() | |
258 | |
259 def initLog(): | |
260 # Init our logging | |
261 global log | |
262 log = logging.getLogger("amaKode") | |
263 # Default to warts and all logging | |
264 log.setLevel(logging.DEBUG) | |
265 | |
266 # Log to this file | |
267 logfile = logging.handlers.RotatingFileHandler(filename = "/tmp/amakode.log", | |
268 maxBytes = 10000, backupCount = 3) | |
269 | |
270 # And stderr | |
271 logstderr = logging.StreamHandler() | |
272 | |
273 # Format it nicely | |
274 formatter = logging.Formatter("[%(name)s] %(message)s") | |
275 | |
276 # Glue it all together | |
277 logfile.setFormatter(formatter) | |
278 logstderr.setFormatter(formatter) | |
279 log.addHandler(logfile) | |
280 log.addHandler(logstderr) | |
281 return(log) | |
282 | |
283 def reportJob(job): | |
284 """ Report to amarok that the job is done """ | |
285 if (job.errormsg == None): | |
286 log.debug("Job " + job.inurl + " completed successfully") | |
287 log.debug("dcop amarok mediabrowser transcodingFinished " + job.inurl + " " + job.outurl) | |
288 else: | |
289 log.debug("Job " + job.inurl + " failed - " + job.errormsg) | |
290 log.debug("dcop amarok mediabrowser transcodingFinished " + job.inurl + "\"\"") | |
291 | |
292 if __name__ == "__main__": | |
293 initLog() | |
294 signal.signal(signal.SIGINT, onStop) | |
295 signal.signal(signal.SIGHUP, onStop) | |
296 signal.signal(signal.SIGTERM, onStop) | |
297 if 1: | |
298 app = amaKode(sys.argv) | |
299 else: | |
300 q = QueueMgr(reportJob) | |
301 j = TranscodeJob("file:///tmp/test.mp3", "ogg") | |
302 q.add(j) | |
303 j2 = TranscodeJob("file:///tmp/test2.mp3", "m4a") | |
304 q.add(j2) | |
305 while not q.isidle(): | |
306 q.poll() | |
307 res = select.select([], [], [], 1) | |
308 | |
309 log.debug("jobs all done") |