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")