]> diplodocus.org Git - flac-archive/blob - flac2mp3
Fix pics nonsense.
[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 main(argv):
208 # Control the exit code for any uncaught exceptions.
209 try:
210 parser = OptionParser()
211 parser.disable_interspersed_args()
212 parser.add_option('-X', '--debug', action='store_true', default=False)
213 parser.add_option('-j', '--jobs', type='int', default=1)
214 parser.add_option('--lame-options')
215 parser.add_option('-q', '--quiet', action='store_true', default=False)
216 parser.add_option('-v', '--verbose', action='store_true', default=False)
217 except:
218 traceback.print_exc()
219 return 2
220
221 try:
222 # Raises SystemExit on invalid options in argv.
223 (options, args) = parser.parse_args(argv[1:])
224 except Exception, error:
225 if isinstance(error, SystemExit):
226 return 1
227 traceback.print_exc()
228 return 2
229
230 try:
231 global debug, flac_options, lame_options, quiet, verbose
232 debug = options.debug
233 lame_options = options.lame_options
234 quiet = options.quiet
235 verbose = options.verbose
236
237 jobs = []
238 for fn in args:
239 try:
240 args = get_decode_args(fn)
241
242 tags = get_tags(fn)
243 album = tags.gets('ALBUM')
244 discnum = tags.gets('DISCNUMBER')
245 track = tags.gets('TRACKNUMBER')
246
247 # lame doesn't seem to support disc number.
248 if discnum != None:
249 album = '%s (disc %s)' % (album, discnum)
250
251 # Stupid hack: only a single-track file should have the
252 # TRACKNUMBER tag, so use it if set for the first pass through
253 # the loop. At the end of the loop, we'll set $track for the
254 # next run, so this continues to work for multi-track files.
255 if track == None:
256 track = 1
257 else:
258 track = int(track)
259
260 pics = flac.get_pictures(fn)
261
262 for i in range(len(tags)):
263 title = tags.gets('TITLE', track)
264 part = tags.gets('PART', track)
265 if part != None:
266 title = '%s - %s' % (title, part)
267 jobs.append([fn, title,
268 tags.gets('ARTIST', track),
269 album,
270 tags.gets('DATE', track),
271 track, args[i], pics])
272 track = i + 2
273 except Exception, error:
274 sys.stderr.write(getattr(error, 'msg', ''))
275 traceback.print_exc()
276 sys.stderr.write('Continuing...\n')
277
278 def getjob(reap):
279 try:
280 job = jobs.pop(0)
281 except IndexError:
282 return
283 return lambda: flac2mp3(*job)
284 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
285 except Exception, error:
286 if isinstance(error, SystemExit):
287 raise
288 # check all print_exc and format_exc in fa-flacd.py; i think
289 # for some i don't do this msg print check
290 sys.stderr.write(getattr(error, 'msg', ''))
291 traceback.print_exc()
292 return 2
293
294 return 0
295
296 if __name__ == '__main__':
297 sys.exit(main(sys.argv))