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