2 # This file is part of mp3togo
4 # Convert audio files to play on a mp3 player
5 # Manage program options
7 # (c) Simeon Veldstra 2004, 2006 <reallifesim@gmail.com>
9 # This software is free.
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.
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
20 # requires python-pyvorbis and python-id3 and lame
28 import mp3togo.options as options
30 from mp3togo.helpers import helpers
33 class Fail(options.Fail):
36 class Error(Exception):
39 class ErrorUnknownFileType(Error):
42 class ErrorNoFile(Error):
45 class ErrorNoCache(Error):
48 class ErrorBadFormat(Error):
51 class ErrorNoXMMSControl(Error):
54 class ErrorXMMSNotRunning(Error):
57 class ErrorNoDCOP(Error):
60 class ErrorAmarokNotRunning(Error):
63 class ErrorUnlocked(Error):
66 class TaskNotReadyError(Error):
69 class ErrorClusterProtocol(Error):
74 class Options(options.Options):
75 """Subclass options.Options and fill in application specific
77 def __init__(self, argv=None, conffile=None, readconf=True):
78 options.Options.__init__(self, '~/.mp3togo')
81 # ((name, short option, long option, default value, hook, help text), ...)
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 ('noguesstags', '', 'no-guessing-tags', False, None,
94 '''Do not attempt to guess missing tags from the file name.'''),
95 ('maxunits', 'm', 'max-size', '0', self._units,
96 '''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.'''),
97 ('maxsize', '', '', 0L, None, ''),
98 ('maxtempunits', '', 'max-temp-size', '0', self._units,
99 '''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.'''),
100 ('maxtempsize', '', '', 0L, None, ''),
101 ('force', 'F', 'force', False, None,
102 '''Overwrite files if they already exist.'''),
103 ('update', 'u', 'update', False, None,
104 '''Overwrite files if they already exist only if they are older than the source file.'''),
105 ('encoder', 'C', 'encoder', 'lame', None,
106 '''The encoder to use to create the output files. Options are wav, lame or oggenc.'''),
107 ('encopts', 'E', 'encoder-options', '', None,
108 '''Compression options for the encoder.'''),
109 ('compfactor', 'z', 'compression-factor', 18.0, None,
110 '''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.'''),
111 ('makecache', '', 'make-cache', '', self._absfile, '''Make an output file cache at the given path.'''),
112 ('cachesize', '', '', 0L, None, ''),
113 ('cacheunits', '', 'cache-size', '0', self._units, '''The size for the new cache.'''),
114 ('usecache', '', 'use-cache', '', self._absfile, '''Use the cache stored at the given path.'''),
115 ('help', 'h', 'help', False, None, '''Print this message.'''),
116 ('playlist', 'p', 'playlist', '', None,
117 '''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.'''),
118 ('readxmms', 'x', 'import-xmms', False, None,
119 '''Get playlist from running instance of XMMS. (You must have the python-xmms module installed)'''),
120 ('readamarok', '', 'import-amarok', False, None,
121 '''Get playlist from running instance of Amarok.'''),
122 ('playerdir', 'o', 'output-dir', os.curdir, self._absfile,
123 '''Where to write the output files.'''),
124 ('index', '', 'index', False, None,
125 '''Create an index file when in wav output mode.'''),
126 ('nonormal', '', 'no-normalize', False, None,
127 '''Don't normalize the wav file before encoding.'''),
128 ('cluster', '', 'cluster', '', None,
129 '''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/'''),
130 ('clusternolocal', '', 'cluster-no-local-node', False, None,
131 '''Only use the remote nodes for processing files.'''),
132 ('clusterslave', '', 'cluster-slave', False, None,
133 '''Start a worker process for cluster mode. This option is used by the cluster master to start slave processes. Do not use.''')]
135 # Override hook defined in Base class
136 self._conffile = self._absfile
138 # Options to not save to the config file:
139 self._nosave_options += []
141 # Binaries to check for:
142 self.bin['wav'] = True
143 self.bin['lame'] = False
144 self.bin['oggenc'] = False
145 self.bin['mpg321'] = False
146 self.bin['ogg123'] = False
147 self.bin['flac'] = False
148 self.bin['faad'] = False
149 self.bin['metaflac'] = False
150 self.bin['normalize-audio'] = False
151 self.bin['dcop'] = False
153 # What input types are supported on this system?
155 for helper in helpers.values():
156 if helper['action'] == 'decode':
157 bin = helper['cmd'].split()[0]
158 for path in map(lambda x: os.path.join(x, bin),
159 os.environ['PATH'].split(':')):
160 if os.path.exists(path):
161 if isinstance(helper['type'], (tuple, list)):
162 self.types.extend(helper['type'])
164 self.types.append(helper['type'])
167 self.version = mp3togo.version
168 self._help_preamble = """
170 mp3togo [-p playlist] [-o output-dir] [options] [input files]
173 mp3togo is a program for converting audio files of various
174 formats into a common format suitable for use on a portable
175 mp3 player or an mp3 CD. The files to convert can be
176 specified in a playlist file (m3u, pls and plain text are
177 supported) or given as arguments to the program. mp3togo
178 will go through the list and run mpg321, ogg123, flac, faad and
179 lame to decode and reencode the files. The newly encoded
180 files will be written to the destination directory. The
181 software can retain as much of the subdirectory structure
185 # Check for Third party modules:
186 self.mod['ogg.vorbis'] = False
187 self.mod['ID3'] = False
188 self.mod['eyeD3'] = False
189 self.mod['xmms'] = False
191 # Go ahead and read in the data:
192 self.getconf(argv, conffile, readconf)
196 # All hooks are called with the lock held
197 # and must reference the ._d dict directly
198 def _post_hook(self):
199 #Set up default encoder options if not specified:
200 if not self._d['encopts']:
201 self.reset_encoder_options()
203 def _absfile(self, name, value):
204 self._d[name] = absfile(value)
206 def _units(self, name, value):
207 #Calculate raw bytes and set [max|temp|cache]size
209 if value[-1] in ('b', 'B'):
211 if value[-1] in ('m', 'M'):
212 max = long(value[:-1]) * 1048576L
213 elif value[-1] in ('g', 'G'):
214 max = long(value[:-1]) * 1073741824L
215 elif value[-1] in ('k', 'K'):
216 max = long(value[:-1]) * 1024L
220 raise Fail, "Bad %s option: %s " % (name, value)
221 sizename = name.replace('units', 'size')
222 self._d[sizename] = max # 'maxsize'
223 self._d[name] = value # 'maxunits'
225 def _arg_files(self, name, value):
229 if os.path.exists(f):
233 def reset_encoder_options(self):
234 """Reset encoder options to defaults."""
235 if self._d['encoder'] == 'lame':
236 self._d['encopts'] = '--abr 96'
237 elif self._d['encoder'] == 'oggenc':
238 self._d['encopts'] = '-m 96 -M 225 -b 100'
239 elif self._d['encoder'] == 'wav':
240 self._d['encopts'] = ''
242 def cleanfilename(self, name):
243 """Remove nasty characters from a filename"""
244 name = name.replace('"', "'")
245 name = name.replace(':', '.')
246 name = name.replace("?", "")
247 name = name.replace('*', '')
248 name = name.replace('`', "'")
249 name = name.replace('&', '+')
250 name = name.replace(';', '.')
251 name = name.replace('#', '-')
252 name = name.replace('|', '-')
257 def getfiletype(self, filename):
258 """Return the format of an audio file"""
259 # Could use some more magic here
260 ft = os.path.splitext(filename)[1][1:]
261 #if ft not in ('mp3', 'ogg', 'flac', 'wav', 'm4a'):
262 if ft not in self.types:
263 raise ErrorUnknownFileType
266 def checkfile(self, name):
267 """Verify the existence of an audio file"""
268 name = name.replace('\n', '')
269 if not os.path.exists(name):
272 self.getfiletype(name) # Throws exception if unknown
275 def est_decoded_size(self, filename):
276 """Estimate size of decoded wav file."""
277 tp = self.getfiletype(filename)
278 helper = find_helper(tp, 'decode')
279 return os.stat(filename).st_size * helpers[helper]['factor']
285 """Try to print a message to a nonblocking stdout"""
286 for i in range(1, 6):
288 sys.stdout.write(msg)
291 time.sleep(0.01 ** (1.0 / i))
295 name = os.path.expanduser(name)
297 name = os.path.abspath(name)
300 def find_helper(type, action):
301 """Return the name of the best helper that performs 'action' on 'type'
303 Raises KeyError if type or action is unknown."""
304 for helper in helpers.keys():
305 if helpers[helper]['action'] == action and \
306 type in helpers[helper]['type']:
310 def make_args(helper, input, output, args=''):
311 """Substitute for command line args"""
312 esc = {'z': '%', 'i': "###input###", 'o': "###output###", 'a': args}
314 fmt = helpers[helper]['cmd']
315 fmt = fmt.replace('%%', '%z')
322 code = fmt[0] and fmt[0][0]
323 if code in esc.keys():
324 fmt[0] = esc[code] + fmt[0][1:]
328 # This foolishness is to prevent splitting
329 # filenames with whitespace in them.
330 if "###input###" in out:
331 out[out.index("###input###")] = input
332 if "###output###" in out:
333 out[out.index("###output###")] = output