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