]> diplodocus.org Git - flac-archive/blob - flac2mp3
Hm, why just copy front cover PICTUREs from flac to mp3? The flac
[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, date) = [(x == None and 'unknown') or x
66 for x in (title, artist, album, date)]
67 try:
68 (skip_arg, until_arg) = skip_until
69 except ValueError:
70 skip_arg = until_arg = ''
71
72 if quiet:
73 flac_options = '--silent'
74 else:
75 flac_options = ''
76
77 tmp = []
78 global lame_options
79 if lame_options != None:
80 tmp.append(lame_options)
81 else:
82 tmp.append('--preset standard');
83 quiet and tmp.append('--quiet')
84 verbose and tmp.append('--verbose')
85 lame_options = ' '.join(tmp)
86
87 outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
88 track, title)).replace('/', '_')
89
90 # Escape any single quotes ' so we can quote this.
91 (fn, title, artist,
92 album, date) = [x.replace("'", r"'\''")
93 for x in (fn, title, artist, album, date)]
94
95 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
96 track, title)).replace('/', '_')
97
98 run_or_die(3, "flac %s -cd %s %s '%s' | lame --add-id3v2 %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d - '%s'"
99 % (flac_options, skip_arg or '', until_arg or '', fn,
100 lame_options, title, artist, album, date, track,
101 quoted_outfile))
102
103 if pics != None:
104 taglib.add_apic_frame_to_mp3(outfile, pics)
105
106 return 0
107
108 ################################################################################
109 # The master process
110
111 def tformat(m, s, c):
112 return '%02d:%02d.%02d' % (m, s, c)
113
114 def get_decode_args(fn):
115 l = []
116
117 p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE)
118 for line in (x.rstrip() for x in p.stdout):
119 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
120 if m != None:
121 l.append(map(int, m.groups()))
122 status = p.wait()
123 # XXX dataloss! check status
124
125 args = []
126 for i in xrange(len(l)):
127 arg = ['--skip=' + tformat(*l[i])]
128 try:
129 next = l[i + 1]
130 except IndexError:
131 next = None
132 if next != None:
133 if next[2] == 0:
134 if next[1] == 0:
135 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
136 else:
137 arg.append('--until=' + tformat(next[0], next[1] - 1,
138 74))
139 else:
140 arg.append('--until=' + tformat(next[0], next[1],
141 next[2] - 1))
142
143 args.append(arg)
144
145 # If no cue sheet, stick a dummy in here.
146 if len(args) == 0:
147 args = [[]]
148
149 return args
150
151 # XXX other things should usue this; flac files, for example, should
152 # get PART as part of the filelname, same as mp3s.
153 class Tags(object):
154 def __init__(self):
155 self._tags = {}
156 def __len__(self):
157 return len(self._tags)
158 def get(self, key, track=None):
159 key = key.upper()
160 try:
161 if track == None:
162 return self._tags[None][key]
163 try:
164 return self._tags[track][key]
165 except KeyError:
166 return self._tags[None][key]
167 except KeyError:
168 return None
169 def gets(self, key, track=None):
170 value = self.get(key, track)
171 if value == None:
172 return None
173 return '\n'.join(value)
174 def set(self, key, value, track=None):
175 if track not in self._tags:
176 self._tags[track] = {}
177 if key not in self._tags[track]:
178 self._tags[track][key] = []
179 self._tags[track][key].append(value)
180
181 def get_tags(fn):
182 '''Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
183 in the file FN.'''
184
185 tags = Tags()
186
187 p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE)
188 for line in (x.rstrip() for x in p.stdout):
189 (tag, value) = line.split('=', 1)
190
191 m = re.search(r'\[([0-9]+)]$', tag)
192 if m != None:
193 tag = tag[:m.start()]
194 track = int(m.group(1))
195 else:
196 track = None
197
198 tags.set(tag, value, track)
199 # XXX dataloss! check status
200 status = p.wait()
201
202 return tags
203
204 def find_pics(fn, tags):
205 pics = tags.get('__flac2mp3_PICTURE')
206
207 if not isinstance(pics, list):
208 pics = flac.get_pictures(fn)
209 tags.set('__flac2mp3_PICTURE', pics)
210
211 return pics
212
213 def main(argv):
214 # Control the exit code for any uncaught exceptions.
215 try:
216 parser = OptionParser()
217 parser.disable_interspersed_args()
218 parser.add_option('-X', '--debug', action='store_true', default=False)
219 parser.add_option('-j', '--jobs', type='int', default=1)
220 parser.add_option('--lame-options')
221 parser.add_option('-q', '--quiet', action='store_true', default=False)
222 parser.add_option('-v', '--verbose', action='store_true', default=False)
223 except:
224 traceback.print_exc()
225 return 2
226
227 try:
228 # Raises SystemExit on invalid options in argv.
229 (options, args) = parser.parse_args(argv[1:])
230 except Exception, error:
231 if isinstance(error, SystemExit):
232 return 1
233 traceback.print_exc()
234 return 2
235
236 try:
237 global debug, flac_options, lame_options, quiet, verbose
238 debug = options.debug
239 lame_options = options.lame_options
240 quiet = options.quiet
241 verbose = options.verbose
242
243 jobs = []
244 for fn in args:
245 try:
246 args = get_decode_args(fn)
247
248 tags = get_tags(fn)
249 album = tags.gets('ALBUM')
250 discnum = tags.gets('DISCNUMBER')
251 track = tags.gets('TRACKNUMBER')
252
253 # lame doesn't seem to support disc number.
254 if discnum != None:
255 album = '%s (disc %s)' % (album, discnum)
256
257 # Stupid hack: only a single-track file should have the
258 # TRACKNUMBER tag, so use it if set for the first pass through
259 # the loop. At the end of the loop, we'll set $track for the
260 # next run, so this continues to work for multi-track files.
261 if track == None:
262 track = 1
263 else:
264 track = int(track)
265
266 for i in range(len(tags)):
267 title = tags.gets('TITLE', track)
268 part = tags.gets('PART', track)
269 if part != None:
270 title = '%s - %s' % (title, part)
271 jobs.append([fn, title,
272 tags.gets('ARTIST', track),
273 album,
274 tags.gets('DATE', track),
275 track, args[i], find_pics(fn, tags)])
276 track = i + 2
277 except Exception, error:
278 sys.stderr.write(getattr(error, 'msg', ''))
279 traceback.print_exc()
280 sys.stderr.write('Continuing...\n')
281
282 def getjob(reap):
283 try:
284 job = jobs.pop(0)
285 except IndexError:
286 return
287 return lambda: flac2mp3(*job)
288 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
289 except Exception, error:
290 if isinstance(error, SystemExit):
291 raise
292 # check all print_exc and format_exc in fa-flacd.py; i think
293 # for some i don't do this msg print check
294 sys.stderr.write(getattr(error, 'msg', ''))
295 traceback.print_exc()
296 return 2
297
298 return 0
299
300 if __name__ == '__main__':
301 sys.exit(main(sys.argv))