Version 0.5.3
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, os
24
25 import mp3togo
26 import mp3togo.options as options
27
28 from mp3togo.helpers import helpers
29
30
31 class Fail(options.Fail):
32 pass
33
34 class Error(Exception):
35 pass
36
37 class ErrorUnknownFileType(Error):
38 pass
39
40 class ErrorNoFile(Error):
41 pass
42
43 class ErrorNoCache(Error):
44 pass
45
46 class ErrorBadFormat(Error):
47 pass
48
49 class ErrorNoXMMSControl(Error):
50 pass
51
52 class ErrorXMMSNotRunning(Error):
53 pass
54
55 class ErrorNoDCOP(Error):
56 pass
57
58 class ErrorAmarokNotRunning(Error):
59 pass
60
61 class ErrorUnlocked(Error):
62 pass
63
64 class TaskNotReadyError(Error):
65 pass
66
67 class ErrorClusterProtocol(Error):
68 pass
69
70
71
72 class Options(options.Options):
73 """Subclass options.Options and fill in application specific
74 information."""
75 def __init__(self, argv=None, conffile=None, readconf=True):
76 options.Options.__init__(self, '~/.mp3togo')
77
78 # Options to add:
79 # ((name, short option, long option, default value, hook, help text), ...)
80 self._set += [
81 ('brwarning', 't', 'file-tag', '.2go', None,
82 '''A string appended to the name of the file to indicate it has been recoded.'''),
83 ('tempdir', 'w', 'work-dir', '/tmp', self._absfile,
84 '''The path to store temporary files. This could need several hundred megabytes free.'''),
85 ('treedepth', 'd', 'tree-depth', 1, None,
86 '''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).'''),
87 ('treestructure', '', 'format', '', None,
88 '''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.'''),
89 ('notags', '', 'no-tags', False, None,
90 '''Do not try to write tags to the output files.'''),
91 ('maxunits', 'm', 'max-size', '0', self._units,
92 '''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.'''),
93 ('maxsize', '', '', 0L, None, ''),
94 ('maxtempunits', '', 'max-temp-size', '0', self._units,
95 '''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.'''),
96 ('maxtempsize', '', '', 0L, None, ''),
97 ('force', 'F', 'force', False, None,
98 '''Overwrite files if they already exist.'''),
99 ('encoder', 'C', 'encoder', 'lame', None,
100 '''The encoder to use to create the output files. Options are wav, lame or oggenc.'''),
101 ('encopts', 'E', 'encoder-options', '', None,
102 '''Compression options for the encoder.'''),
103 ('compfactor', 'z', 'compression-factor', 16.0, None,
104 '''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.'''),
105 ('makecache', '', 'make-cache', '', self._absfile, '''Make an output file cache at the given path.'''),
106 ('cachesize', '', '', 0L, None, ''),
107 ('cacheunits', '', 'cache-size', '0', self._units, '''The size for the new cache.'''),
108 ('usecache', '', 'use-cache', '', self._absfile, '''Use the cache stored at the given path.'''),
109 ('help', 'h', 'help', False, None, '''Print this message.'''),
110 ('playlist', 'p', 'playlist', '', None,
111 '''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.'''),
112 ('readxmms', 'x', 'import-xmms', False, None,
113 '''Get playlist from running instance of XMMS. (You must have the python-xmms module installed)'''),
114 ('readamarok', '', 'import-amarok', False, None,
115 '''Get playlist from running instance of Amarok.'''),
116 ('playerdir', 'o', 'output-dir', os.curdir, self._absfile,
117 '''Where to write the output files.'''),
118 ('index', '', 'index', False, None,
119 '''Create an index file when in wav output mode.'''),
120 ('nonormal', '', 'no-normalize', False, None,
121 '''Don't normalize the wav file before encoding.'''),
122 ('cluster', '', 'cluster', '', None,
123 '''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/'''),
124 ('clusternolocal', '', 'cluster-no-local-node', False, None,
125 '''Only use the remote nodes for processing files.'''),
126 ('clusterslave', '', 'cluster-slave', False, None,
127 '''Start a worker process for cluster mode. This option is used by the cluster master to start slave processes. Do not use.''')]
128
129 # Override hook defined in Base class
130 self._conffile = self._absfile
131
132 # Options to not save to the config file:
133 self._nosave_options += []
134
135 # Binaries to check for:
136 self.bin['wav'] = True
137 self.bin['lame'] = False
138 self.bin['oggenc'] = False
139 self.bin['mpg321'] = False
140 self.bin['ogg123'] = False
141 self.bin['flac'] = False
142 self.bin['faad'] = False
143 self.bin['metaflac'] = False
144 self.bin['normalize-audio'] = False
145 self.bin['dcop'] = False
146
147 # What input types are supported on this system?
148 self.types = []
149 for helper in helpers.values():
150 if helper['action'] == 'decode':
151 bin = helper['cmd'].split()[0]
152 for path in map(lambda x: os.path.join(x, bin),
153 os.environ['PATH'].split(':')):
154 if os.path.exists(path):
155 if isinstance(helper['type'], (tuple, list)):
156 self.types.extend(helper['type'])
157 else:
158 self.types.append(helper['type'])
159 break
160
161 self.version = mp3togo.version
162 self._help_preamble = """
163 Synopsis:
164 mp3togo [-p playlist] [-o output-dir] [options] [input files]
165
166 Description:
167 mp3togo is a program for converting audio files of various
168 formats into a common format suitable for use on a portable
169 mp3 player or an mp3 CD. The files to convert can be
170 specified in a playlist file (m3u, pls and plain text are
171 supported) or given as arguments to the program. mp3togo
172 will go through the list and run mpg321, ogg123, flac, faad and
173 lame to decode and reencode the files. The newly encoded
174 files will be written to the destination directory. The
175 software can retain as much of the subdirectory structure
176 as desired.
177 """
178
179 # Check for Third party modules:
180 self.mod['ogg.vorbis'] = False
181 self.mod['ID3'] = False
182 self.mod['xmms'] = False
183
184 # Go ahead and read in the data:
185 self.getconf(argv, conffile, readconf)
186
187
188
189 # All hooks are called with the lock held
190 # and must reference the ._d dict directly
191 def _post_hook(self):
192 #Set up default encoder options if not specified:
193 if not self._d['encopts']:
194 self.reset_encoder_options()
195
196 def _absfile(self, name, value):
197 self._d[name] = absfile(value)
198
199 def _units(self, name, value):
200 #Calculate raw bytes and set [max|temp|cache]size
201 try:
202 if value[-1] in ('m', 'M'):
203 max = long(value[:-1]) * 1048576L
204 elif value[-1] in ('g', 'G'):
205 max = long(value[:-1]) * 1073741824L
206 elif value[-1] in ('k', 'K'):
207 max = long(value[:-1]) * 1024L
208 else:
209 max = long(value)
210 except ValueError:
211 raise Fail, "Bad %s option: %s " % (name, value)
212 sizename = name.replace('units', 'size')
213 self._d[sizename] = max # 'maxsize'
214 self._d[name] = value # 'maxunits'
215
216 def _arg_files(self, name, value):
217 l = []
218 for f in value:
219 f = absfile(f)
220 if os.path.exists(f):
221 l.append(f)
222 self._d[name] = l
223
224 def reset_encoder_options(self):
225 """Reset encoder options to defaults."""
226 if self._d['encoder'] == 'lame':
227 self._d['encopts'] = '--abr 96'
228 elif self._d['encoder'] == 'oggenc':
229 self._d['encopts'] = '-m 96 -M 225 -b 100'
230 elif self._d['encoder'] == 'wav':
231 self._d['encopts'] = ''
232
233 def cleanfilename(self, name):
234 """Remove nasty characters from a filename"""
235 name = name.replace('"', "'")
236 name = name.replace(':', '.')
237 name = name.replace("?", "")
238 name = name.replace('*', '')
239 name = name.replace('`', "'")
240 name = name.replace('&', '+')
241 name = name.replace(';', '.')
242 name = name.replace('#', '-')
243 name = name.replace('|', '-')
244 if name[0] == '.':
245 name = '_' + name
246 return name
247
248 def getfiletype(self, filename):
249 """Return the format of an audio file"""
250 # Could use some more magic here
251 ft = os.path.splitext(filename)[1][1:]
252 #if ft not in ('mp3', 'ogg', 'flac', 'wav', 'm4a'):
253 if ft not in self.types:
254 raise ErrorUnknownFileType
255 return ft
256
257 def checkfile(self, name):
258 """Verify the existence of an audio file"""
259 name = name.replace('\n', '')
260 if not os.path.exists(name):
261 raise ErrorNoFile
262 name = absfile(name)
263 self.getfiletype(name) # Throws exception if unknown
264 return name
265
266 def est_decoded_size(self, filename):
267 """Estimate size of decoded wav file."""
268 tp = self.getfiletype(filename)
269 helper = find_helper(tp, 'decode')
270 return os.stat(filename).st_size * helpers[helper]['factor']
271
272
273
274
275 def try_print(msg):
276 """Try to print a message to a nonblocking stdout"""
277 for i in range(5):
278 try:
279 sys.stdout.write(msg)
280 return
281 except:
282 time.sleep(0.01 ** (1.0 / i))
283
284 def absfile(name):
285 name = name.strip()
286 name = os.path.expanduser(name)
287 if name:
288 name = os.path.abspath(name)
289 return name
290
291 def find_helper(type, action):
292 """Return the name of the best helper that performs 'action' on 'type'
293
294 Raises KeyError if type or action is unknown."""
295 for helper in helpers.keys():
296 if helpers[helper]['action'] == action and \
297 type in helpers[helper]['type']:
298 return helper
299 raise KeyError
300
301 def make_args(helper, input, output, args=''):
302 """Substitute for command line args"""
303 esc = {'z': '%', 'i': "###input###", 'o': "###output###", 'a': args}
304 out = ""
305 fmt = helpers[helper]['cmd']
306 fmt = fmt.replace('%%', '%z')
307 fmt = fmt.split('%')
308 while fmt:
309 out += fmt[0]
310 if len(fmt) <= 1:
311 break
312 fmt = fmt[1:]
313 code = fmt[0] and fmt[0][0]
314 if code in esc.keys():
315 fmt[0] = esc[code] + fmt[0][1:]
316 else:
317 raise ErrorBadFormat
318 out = out.split()
319 # This foolishness is to prevent splitting
320 # filenames with whitespace in them.
321 if "###input###" in out:
322 out[out.index("###input###")] = input
323 if "###output###" in out:
324 out[out.index("###output###")] = output
325 return out
326
327