]> diplodocus.org Git - flac-archive/blob - flac2mp3
Set encoding of description.
[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 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 pic != None:
104 taglib.add_apic_frame_to_mp3(outfile, pic[0], pic[1], pic[2])
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_pic(fn, tags):
205 pic = tags.get('__flac2mp3_PICTURE')
206
207 if not isinstance(pic, tuple):
208 for i in flac.get_pictures(fn):
209 if i[1] == flac.PICTURE_TYPE_FRONT_COVER:
210 pic = i[:3]
211 break
212 tags.set('__flac2mp3_PICTURE', pic)
213
214 return pic
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_pic(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))