Merge with bugfix-0.5 0.5.12
mp3togo/conf.py
1 # - conf.py -
2 # This file is part of mp3togo
3
4 # Convert audio files to play on a mp3 player
5 # Manage program options
6 #
7 # (c) Simeon Veldstra 2004, 2006 <reallifesim@gmail.com>
8 #
9 # This software is free.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You may redistribute this program under the terms of the
17 # GNU General Public Licence version 2
18 # Available in this package or at http://www.fsf.org
19
20 # requires python-pyvorbis and python-id3 and lame
21 # and mpg321
22
23 import sys
24 import os
25 import time
26
27 import mp3togo
28 import mp3togo.options as options
29
30 from mp3togo.helpers import helpers
31
32
33 class Fail(options.Fail):
34 pass
35
36 class Error(Exception):
37 pass
38
39 class ErrorUnknownFileType(Error):
40 pass
41
42 class ErrorNoFile(Error):
43 pass
44
45 class ErrorNoCache(Error):
46 pass
47
48 class ErrorBadFormat(Error):
49 pass
50
51 class ErrorNoXMMSControl(Error):
52 pass
53
54 class ErrorXMMSNotRunning(Error):
55 pass
56
57 class ErrorNoDCOP(Error):
58 pass
59
60 class ErrorAmarokNotRunning(Error):
61 pass
62
63 class ErrorUnlocked(Error):
64 pass
65
66 class TaskNotReadyError(Error):
67 pass
68
69 class ErrorClusterProtocol(Error):
70 pass
71
72
73
74 class Options(options.Options):
75 """Subclass options.Options and fill in application specific
76 information."""
77 def __init__(self, argv=None, conffile=None, readconf=True):
78 self.argv = argv
79 options.Options.__init__(self, absfile('~/.mp3togo'))
80
81 # Options to add:
82 # ((name, short option, long option, default value, hook, help text), ...)
83 self._set += [
84 ('gui', 'X', 'gui-mode', False, None,
85 '''Start mp3togo GTK interface.'''),
86 ('brwarning', 't', 'file-tag', '.2go', None,
87 '''A string appended to the name of the file to indicate it has been recoded.'''),
88 ('tempdir', 'w', 'work-dir', '/tmp', self._absfile,
89 '''The path to store temporary files. This could need several hundred megabytes free.'''),
90 ('treedepth', 'd', 'tree-depth', 1, None,
91 '''The number of directory levels to preserve in the output tree. Use 0 to put all output files directly into the target directory. Use 2 to create a directory for the Artist and then the Album (Assuming your music collection is organized in directories by artist then album).'''),
92 ('treestructure', '', 'format', '', None,
93 '''A string specifying the format for the output files. The format string can contain the following escapes:<li>%a - Artist<li>%l - Album<li>%t - Title<li>%y - Year<li>%g - Genre<li>%% - Literal '%'<br>Overrides --tree-depth.'''),
94 ('notags', '', 'no-tags', False, None,
95 '''Do not try to write tags to the output files.'''),
96 ('noguesstags', '', 'no-guessing-tags', False, None,
97 '''Do not attempt to guess missing tags from the file name.'''),
98 ('maxunits', 'm', 'max-size', '0', self._units,
99 '''The disk space available to use in bytes. Append 'M' for megabytes, 'G' for gigabytes or 'K' for kilobytes. Use 0 to use all available space on the filesystem.'''),
100 ('maxsize', '', '', 0L, None, ''),
101 ('maxtempunits', '', 'max-temp-size', '0', self._units,
102 '''The disk space available to use for temporary files in bytes. Append 'M' for megabytes, 'G' for gigabytes or 'K' for kilobytes. Use 0 to use all available space on the filesystem.'''),
103 ('maxtempsize', '', '', 0L, None, ''),
104 ('force', 'F', 'force', False, None,
105 '''Overwrite files if they already exist.'''),
106 ('update', 'u', 'update', False, None,
107 '''Overwrite files if they already exist only if they are older than the source file.'''),
108 ('encoder', 'C', 'encoder', 'lame', None,
109 '''The encoder to use to create the output files. Options are wav, lame or oggenc.'''),
110 ('encopts', 'E', 'encoder-options', '', None,
111 '''Compression options for the encoder.'''),
112 ('compfactor', 'z', 'compression-factor', 18.0, None,
113 '''If you change the lame options, you must change this factor to match the compression rate of the new options. This is used to estimate completed file size.'''),
114 ('makecache', '', 'make-cache', '', self._absfile, '''Make an output file cache at the given path.'''),
115 ('cachesize', '', '', 0L, None, ''),
116 ('cacheunits', '', 'cache-size', '0', self._units, '''The size for the new cache.'''),
117 ('usecache', '', 'use-cache', '', self._absfile, '''Use the cache stored at the given path.'''),
118 ('help', 'h', 'help', False, None, '''Print this message.'''),
119 ('playlist', 'p', 'playlist', '', None,
120 '''The playlist to convert. Playlists can be simple lists of file paths, one per line, or .m3u files or the native XMMS playlist file format. Playlists can be also listed on the command line as free arguments after all of the options if they have a recognizable extension. Currently .m3u and .pls are recognized.'''),
121 ('readxmms', 'x', 'import-xmms', False, None,
122 '''Get playlist from running instance of XMMS. (You must have the python-xmms module installed)'''),
123 ('readamarok', '', 'import-amarok', False, None,
124 '''Get playlist from running instance of Amarok.'''),
125 ('playerdir', 'o', 'output-dir', os.curdir, self._absfile,
126 '''Where to write the output files.'''),
127 ('index', '', 'index', False, None,
128 '''Create an index file when in wav output mode.'''),
129 ('nonormal', '', 'no-normalize', False, None,
130 '''Don't normalize the wav file before encoding.'''),
131 ('cluster', '', 'cluster', '', None,
132 '''Manage a cluster of worker machines. Cluster mode distributes tracks between multiple computers connected to a LAN. Provide a comma separated list of IP addresses or hostnames. The master machine must be able to log in to the slaves without a password and the slaves must have shared access to the filesystem. See the website for an example of how to set it up.<li>http://puddle.ca/mp3togo/'''),
133 ('clusternolocal', '', 'cluster-no-local-node', False, None,
134 '''Only use the remote nodes for processing files.'''),
135 ('clusterslave', '', 'cluster-slave', False, None,
136 '''Start a worker process for cluster mode. This option is used by the cluster master to start slave processes. Do not use.''')]
137
138 # Override hook defined in Base class
139 self._conffile = self._absfile
140
141 # Options to not save to the config file:
142 self._nosave_options += []
143
144 # Binaries to check for:
145 self.bin['wav'] = True
146 self.bin['lame'] = False
147 self.bin['oggenc'] = False
148 self.bin['mpg321'] = False
149 self.bin['ogg123'] = False
150 self.bin['flac'] = False
151 self.bin['faad'] = False
152 self.bin['metaflac'] = False
153 self.bin['normalize-audio'] = False
154 self.bin['dcop'] = False
155
156 # What input types are supported on this system?
157 self.types = []
158 for helper in helpers.values():
159 if helper['action'] == 'decode':
160 bin = helper['cmd'].split()[0]
161 for path in map(lambda x: os.path.join(x, bin),
162 os.environ['PATH'].split(':')):
163 if os.path.exists(path):
164 if isinstance(helper['type'], (tuple, list)):
165 self.types.extend(helper['type'])
166 else:
167 self.types.append(helper['type'])
168 break
169
170 self.version = mp3togo.version
171 self._help_preamble = """
172 Synopsis:
173 mp3togo [-p playlist] [-o output-dir] [options] [input files]
174
175 Description:
176 mp3togo is a program for converting audio files of various
177 formats into a common format suitable for use on a portable
178 mp3 player or an mp3 CD. The files to convert can be
179 specified in a playlist file (m3u, pls and plain text are
180 supported) or given as arguments to the program. mp3togo
181 will go through the list and run mpg321, ogg123, flac, faad and
182 lame to decode and reencode the files. The newly encoded
183 files will be written to the destination directory. The
184 software can retain as much of the subdirectory structure
185 as desired.
186 """
187
188 # Check for Third party modules:
189 self.mod['ogg.vorbis'] = False
190 self.mod['ID3'] = False
191 self.mod['eyeD3'] = False
192 self.mod['xmms'] = False
193
194 # Go ahead and read in the data:
195 self.getconf(argv, conffile, readconf)
196
197
198
199 # All hooks are called with the lock held
200 # and must reference the ._d dict directly
201 def _post_hook(self):
202 #Set up default encoder options if not specified:
203 if not self._d['encopts']:
204 self.reset_encoder_options()
205
206 def _absfile(self, name, value):
207 self._d[name] = absfile(value)
208
209 def _units(self, name, value):
210 #Calculate raw bytes and set [max|temp|cache]size
211 try:
212 if value[-1] in ('b', 'B'):
213 value = value[:-1]
214 if value[-1] in ('m', 'M'):
215 max = long(value[:-1]) * 1048576L
216 elif value[-1] in ('g', 'G'):
217 max = long(value[:-1]) * 1073741824L
218 elif value[-1] in ('k', 'K'):
219 max = long(value[:-1]) * 1024L
220 else:
221 max = long(value)
222 except ValueError:
223 raise Fail, "Bad %s option: %s " % (name, value)
224 sizename = name.replace('units', 'size')
225 self._d[sizename] = max # 'maxsize'
226 self._d[name] = value # 'maxunits'
227
228 def _arg_files(self, name, value):
229 l = []
230 for f in value:
231 f = absfile(f)
232 if os.path.exists(f):
233 l.append(f)
234 self._d[name] = l
235
236 def reset_encoder_options(self):
237 """Reset encoder options to defaults."""
238 if self._d['encoder'] == 'lame':
239 self._d['encopts'] = '--abr 96'
240 elif self._d['encoder'] == 'oggenc':
241 self._d['encopts'] = '-m 96 -M 225 -b 100'
242 elif self._d['encoder'] == 'wav':
243 self._d['encopts'] = ''
244
245 def cleanfilename(self, name):
246 """Remove nasty characters from a filename"""
247 name = name.replace('"', "'")
248 name = name.replace(':', '.')
249 name = name.replace("?", "")
250 name = name.replace('*', '')
251 name = name.replace('`', "'")
252 name = name.replace('&', '+')
253 name = name.replace(';', '.')
254 name = name.replace('#', '-')
255 name = name.replace('|', '-')
256 if name[0] == '.':
257 name = '_' + name
258 return name
259
260 def getfiletype(self, filename):
261 """Return the format of an audio file"""
262 # Could use some more magic here
263 ft = os.path.splitext(filename)[1][1:]
264 #if ft not in ('mp3', 'ogg', 'flac', 'wav', 'm4a'):
265 if ft not in self.types:
266 raise ErrorUnknownFileType
267 return ft
268
269 def checkfile(self, name):
270 """Verify the existence of an audio file"""
271 name = name.replace('\n', '')
272 name = name.replace('\r', '')
273 name = name.replace('\f', '')
274 if not os.path.exists(name):
275 raise ErrorNoFile
276 name = absfile(name)
277 self.getfiletype(name) # Throws exception if unknown
278 return name
279
280 def est_decoded_size(self, filename):
281 """Estimate size of decoded wav file."""
282 tp = self.getfiletype(filename)
283 helper = find_helper(tp, 'decode')
284 return os.stat(filename).st_size * helpers[helper]['factor']
285
286
287
288
289 def try_print(msg):
290 """Try to print a message to a nonblocking stdout"""
291 for i in range(1, 6):
292 try:
293 sys.stdout.write(msg)
294 return
295 except:
296 time.sleep(0.01 ** (1.0 / i))
297
298 def absfile(name):
299 name = name.strip()
300 name = os.path.expanduser(name)
301 if name:
302 name = os.path.abspath(name)
303 return name
304
305 def find_helper(type, action):
306 """Return the name of the best helper that performs 'action' on 'type'
307
308 Raises KeyError if type or action is unknown."""
309 for helper in helpers.keys():
310 if helpers[helper]['action'] == action and \
311 type in helpers[helper]['type']:
312 return helper
313 raise KeyError
314
315 def make_args(helper, input, output, args=''):
316 """Substitute for command line args"""
317 esc = {'z': '%', 'i': "###input###", 'o': "###output###", 'a': args}
318 out = ""
319 fmt = helpers[helper]['cmd']
320 fmt = fmt.replace('%%', '%z')
321 fmt = fmt.split('%')
322 while fmt:
323 out += fmt[0]
324 if len(fmt) <= 1:
325 break
326 fmt = fmt[1:]
327 code = fmt[0] and fmt[0][0]
328 if code in esc.keys():
329 fmt[0] = esc[code] + fmt[0][1:]
330 else:
331 raise ErrorBadFormat
332 out = out.split()
333 # This foolishness is to prevent splitting
334 # filenames with whitespace in them.
335 if "###input###" in out:
336 out[out.index("###input###")] = input
337 if "###output###" in out:
338 out[out.index("###output###")] = output
339 return out
340
341