]> diplodocus.org Git - flac-archive/blob - flac2mp3
(flac2mp3): Fix skip_until processing: stop pointless splitting the
[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 ''' #' # python-mode is sucks
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 return len(self._tags)
157 def get(self, key, track=None):
158 key = key.upper()
159 try:
160 try:
161 return self._tags[track][key]
162 except KeyError:
163 return self._global[key]
164 except KeyError:
165 return None
166 def gets(self, key, track=None):
167 value = self.get(key, track)
168 if value == None:
169 return None
170 return '\n'.join(value)
171 def set(self, key, value, track=None):
172 key = key.upper()
173 if track == None:
174 tags = self._global
175 else:
176 try:
177 tags = self._tags[track]
178 except KeyError:
179 tags = self._tags[track] = {}
180 if key not in tags:
181 tags[key] = []
182 tags[key].append(value)
183
184 def get_tags(fn):
185 '''Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
186 in the file FN.'''
187
188 tags = Tags()
189
190 p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE)
191 for line in (x.rstrip() for x in p.stdout):
192 (tag, value) = line.split('=', 1)
193
194 m = re.search(r'\[([0-9]+)]$', tag)
195 if m != None:
196 tag = tag[:m.start()]
197 track = int(m.group(1))
198 else:
199 track = None
200
201 tags.set(tag, value, track)
202 # XXX dataloss! check status
203 status = p.wait()
204
205 return tags
206
207 def find_pics(fn, tags):
208 pics = tags.get('__flac2mp3_PICTURE')
209
210 if not isinstance(pics, list):
211 pics = flac.get_pictures(fn)
212 tags.set('__flac2mp3_PICTURE', pics)
213
214 return pics
215
216 def main(argv):
217 # Control the exit code for any uncaught exceptions.
218 try:
219 parser = OptionParser()
220 parser.disable_interspersed_args()
221 parser.add_option('-X', '--debug', action='store_true', default=False)
222 parser.add_option('-j', '--jobs', type='int', default=1)
223 parser.add_option('--lame-options')
224 parser.add_option('-q', '--quiet', action='store_true', default=False)
225 parser.add_option('-v', '--verbose', action='store_true', default=False)
226 except:
227 traceback.print_exc()
228 return 2
229
230 try:
231 # Raises SystemExit on invalid options in argv.
232 (options, args) = parser.parse_args(argv[1:])
233 except Exception, error:
234 if isinstance(error, SystemExit):
235 return 1
236 traceback.print_exc()
237 return 2
238
239 try:
240 global debug, flac_options, lame_options, quiet, verbose
241 debug = options.debug
242 lame_options = options.lame_options
243 quiet = options.quiet
244 verbose = options.verbose
245
246 jobs = []
247 for fn in args:
248 try:
249 args = get_decode_args(fn)
250
251 tags = get_tags(fn)
252 album = tags.gets('ALBUM')
253 discnum = tags.gets('DISCNUMBER')
254 track = tags.gets('TRACKNUMBER')
255
256 # lame doesn't seem to support disc number.
257 if discnum != None:
258 album = '%s (disc %s)' % (album, discnum)
259
260 # Stupid hack: only a single-track file should have the
261 # TRACKNUMBER tag, so use it if set for the first pass through
262 # the loop. At the end of the loop, we'll set $track for the
263 # next run, so this continues to work for multi-track files.
264 if track == None:
265 track = 1
266 else:
267 track = int(track)
268
269 for i in range(len(tags)):
270 title = tags.gets('TITLE', track)
271 part = tags.gets('PART', track)
272 if part != None:
273 title = '%s - %s' % (title, part)
274 jobs.append([fn, title,
275 tags.gets('ARTIST', track),
276 album,
277 tags.gets('DATE', track),
278 track, args[i], find_pics(fn, tags)])
279 track = i + 2
280 except Exception, error:
281 sys.stderr.write(getattr(error, 'msg', ''))
282 traceback.print_exc()
283 sys.stderr.write('Continuing...\n')
284
285 def getjob(reap):
286 try:
287 job = jobs.pop(0)
288 except IndexError:
289 return
290 return lambda: flac2mp3(*job)
291 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
292 except Exception, error:
293 if isinstance(error, SystemExit):
294 raise
295 # check all print_exc and format_exc in fa-flacd.py; i think
296 # for some i don't do this msg print check
297 sys.stderr.write(getattr(error, 'msg', ''))
298 traceback.print_exc()
299 return 2
300
301 return 0
302
303 if __name__ == '__main__':
304 sys.exit(main(sys.argv))