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