]> diplodocus.org Git - flac-archive/blob - flac2mp3
pointless quote change
[flac-archive] / flac2mp3
1 #! /usr/bin/env python2.4
2
3 """
4 =head1 NAME
5
6 B<flac2mp3> - transcode FLAC file to MP3 files
7
8 =head1 SYNOPSIS
9
10 B<flac2mp3> [B<--lame-options> I<lame-options>] [B<-j> I<jobs>] [B<-q>] [B<-v>] I<file> [...]
11
12 =head1 DESCRIPTION
13
14 B<flac2mp3> transcodes the FLAC files I<file> to MP3 files. I<file>
15 may be the kind of FLAC file B<fa-flacd> generates. That is, it
16 contains a cue sheet, one TITLE tag per track listed therein, and
17 ARTIST, ALBUM, and DATE tags.
18
19 =head1 OPTIONS
20
21 =over 4
22
23 =item B<--lame-options> I<lame-options>
24
25 Pass I<lame-options> to B<lame>. This ends up being passed to the
26 shell, so feel free to take advantage of that. You'll almost
27 certainly have to put I<lame-options> in single quotes.
28
29 =item B<-j> [B<--jobs>] I<jobs>
30
31 Run up to I<jobs> jobs instead of the default 1.
32
33 =item B<-q> [B<--quiet>]
34
35 Suppress status information. This option is passed along to B<flac>
36 and B<lame>.
37
38 =item B<-v> [B<--verbose>]
39
40 Print diagnostic information. This option is passed along to B<flac>
41 and B<lame>.
42
43 =back
44
45 =head1 AUTHORS
46
47 Written by Eric Gillespie <epg@pretzelnet.org>.
48
49 =cut
50
51 """
52
53 import re, sys, traceback
54 from optparse import OptionParser
55 from subprocess import Popen, PIPE
56
57 import org.diplodocus.jobs
58 from org.diplodocus import flac, taglib
59 from org.diplodocus.util import run_or_die
60
61 ################################################################################
62 # The child processes
63
64 def flac2mp3(fn, title, artist, album, date, track, skip_until, pics=None):
65 (title, artist, album) = [(x == None and 'unknown') or x
66 for x in (title, artist, album)]
67 if date == None:
68 date = ''
69
70 if quiet:
71 flac_options = '--silent'
72 else:
73 flac_options = ''
74
75 tmp = []
76 global lame_options
77 if lame_options != None:
78 tmp.append(lame_options)
79 else:
80 tmp.append('--preset standard');
81 quiet and tmp.append('--quiet')
82 verbose and tmp.append('--verbose')
83 lame_options = ' '.join(tmp)
84
85 outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
86 track, title)).replace('/', '_')
87
88 # Escape any single quotes ' so we can quote this.
89 (fn, title, artist,
90 album, date) = [x.replace("'", r"'\''")
91 for x in (fn, title, artist, album, date)]
92
93 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
94 track, title)).replace('/', '_')
95
96 run_or_die(3, "flac %s -cd %s '%s' | lame --add-id3v2 %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d - '%s'"
97 % (flac_options, ' '.join(skip_until), fn,
98 lame_options, title, artist, album, date, track,
99 quoted_outfile))
100
101 if pics != None:
102 taglib.add_apic_frame_to_mp3(outfile, pics)
103
104 return 0
105
106 ################################################################################
107 # The master process
108
109 def tformat(m, s, c):
110 return '%02d:%02d.%02d' % (m, s, c)
111
112 def get_decode_args(fn):
113 l = []
114
115 p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE)
116 for line in (x.rstrip() for x in p.stdout):
117 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
118 if m != None:
119 l.append(map(int, m.groups()))
120 status = p.wait()
121 # XXX dataloss! check status
122
123 args = []
124 for i in xrange(len(l)):
125 arg = ['--skip=' + tformat(*l[i])]
126 try:
127 next = l[i + 1]
128 except IndexError:
129 next = None
130 if next != None:
131 if next[2] == 0:
132 if next[1] == 0:
133 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
134 else:
135 arg.append('--until=' + tformat(next[0], next[1] - 1,
136 74))
137 else:
138 arg.append('--until=' + tformat(next[0], next[1],
139 next[2] - 1))
140
141 args.append(arg)
142
143 # If no cue sheet, stick a dummy in here.
144 if len(args) == 0:
145 args = [[]]
146
147 return args
148
149 # XXX other things should usue this; flac files, for example, should
150 # get PART as part of the filelname, same as mp3s.
151 class Tags(object):
152 def __init__(self):
153 self._global = {}
154 self._tags = {}
155 def __len__(self):
156 # All files have at least one track.
157 return max(1, len(self._tags))
158 def get(self, key, track=None):
159 key = key.upper()
160 try:
161 try:
162 return self._tags[track][key]
163 except KeyError:
164 return self._global[key]
165 except KeyError:
166 return None
167 def gets(self, key, track=None):
168 value = self.get(key, track)
169 if value == None:
170 return None
171 return '\n'.join(value)
172 def set(self, key, value, track=None):
173 key = key.upper()
174 if track == None:
175 tags = self._global
176 else:
177 try:
178 tags = self._tags[track]
179 except KeyError:
180 tags = self._tags[track] = {}
181 if key not in tags:
182 tags[key] = []
183 tags[key].append(value)
184
185 def get_tags(fn):
186 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
187 in the file FN."""
188
189 tags = Tags()
190
191 p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE)
192 for line in (x.rstrip() for x in p.stdout):
193 (tag, value) = line.split('=', 1)
194
195 m = re.search(r'\[([0-9]+)]$', tag)
196 if m != None:
197 tag = tag[:m.start()]
198 track = int(m.group(1))
199 else:
200 track = None
201
202 tags.set(tag, value, track)
203 # XXX dataloss! check status
204 status = p.wait()
205
206 return tags
207
208 def main(argv):
209 # Control the exit code for any uncaught exceptions.
210 try:
211 parser = OptionParser()
212 parser.disable_interspersed_args()
213 parser.add_option('-X', '--debug', action='store_true', default=False)
214 parser.add_option('-j', '--jobs', type='int', default=1)
215 parser.add_option('--lame-options')
216 parser.add_option('-q', '--quiet', action='store_true', default=False)
217 parser.add_option('-v', '--verbose', action='store_true', default=False)
218 except:
219 traceback.print_exc()
220 return 2
221
222 try:
223 # Raises SystemExit on invalid options in argv.
224 (options, args) = parser.parse_args(argv[1:])
225 except Exception, error:
226 if isinstance(error, SystemExit):
227 return 1
228 traceback.print_exc()
229 return 2
230
231 try:
232 global debug, flac_options, lame_options, quiet, verbose
233 debug = options.debug
234 lame_options = options.lame_options
235 quiet = options.quiet
236 verbose = options.verbose
237
238 jobs = []
239 for fn in args:
240 try:
241 args = get_decode_args(fn)
242
243 tags = get_tags(fn)
244 album = tags.gets('ALBUM')
245 discnum = tags.gets('DISCNUMBER')
246 track = tags.gets('TRACKNUMBER')
247
248 # lame doesn't seem to support disc number.
249 if discnum != None:
250 album = '%s (disc %s)' % (album, discnum)
251
252 # Stupid hack: only a single-track file should have the
253 # TRACKNUMBER tag, so use it if set for the first pass through
254 # the loop. At the end of the loop, we'll set $track for the
255 # next run, so this continues to work for multi-track files.
256 if track == None:
257 track = 1
258 else:
259 track = int(track)
260
261 pics = flac.get_pictures(fn)
262
263 for i in range(len(tags)):
264 title = tags.gets('TITLE', track)
265 part = tags.gets('PART', track)
266 if part != None:
267 title = '%s - %s' % (title, part)
268 jobs.append([fn, title,
269 tags.gets('ARTIST', track),
270 album,
271 tags.gets('DATE', track),
272 track, args[i], pics])
273 track = i + 2
274 except Exception, error:
275 sys.stderr.write(getattr(error, 'msg', ''))
276 traceback.print_exc()
277 sys.stderr.write('Continuing...\n')
278
279 def getjob(reap):
280 try:
281 job = jobs.pop(0)
282 except IndexError:
283 return
284 return lambda: flac2mp3(*job)
285 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
286 except Exception, error:
287 if isinstance(error, SystemExit):
288 raise
289 # check all print_exc and format_exc in fa-flacd.py; i think
290 # for some i don't do this msg print check
291 sys.stderr.write(getattr(error, 'msg', ''))
292 traceback.print_exc()
293 return 2
294
295 return 0
296
297 if __name__ == '__main__':
298 sys.exit(main(sys.argv))