]> diplodocus.org Git - flac-archive/blob - flac2mp3
Oops, abs(status) in SubprocessError.
[flac-archive] / flac2mp3
1 #!/usr/bin/python
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 Note that lame is retarded, and parses B<LANG> directly itself! So, in order
20 for it to transcode textual tags, you must specify the encoding in LANG, e.g.
21 LANG=en_US.utf-8
22
23 =head1 OPTIONS
24
25 =over 4
26
27 =item B<--lame-options> I<lame-options>
28
29 Pass I<lame-options> to B<lame>. This ends up being passed to the
30 shell, so feel free to take advantage of that. You'll almost
31 certainly have to put I<lame-options> in single quotes.
32
33 =item B<-j> [B<--jobs>] I<jobs>
34
35 Run up to I<jobs> jobs instead of the default 1.
36
37 =item B<-q> [B<--quiet>]
38
39 Suppress status information. This option is passed along to B<flac>
40 and B<lame>.
41
42 =item B<-v> [B<--verbose>]
43
44 Print diagnostic information. This option is passed along to B<flac>
45 and B<lame>.
46
47 =back
48
49 =head1 AUTHORS
50
51 Written by Eric Gillespie <epg@pretzelnet.org>.
52
53 =cut
54
55 """
56
57 import os, re, sys, tempfile, traceback
58 from optparse import OptionParser
59 from subprocess import Popen, PIPE
60
61 import org.diplodocus.jobs
62 from org.diplodocus.util import run_or_die
63
64 from flac_archive import flac
65 from flac_archive.tags import Tags
66
67 ################################################################################
68 # The child processes
69
70 def flac2mp3(fn, title, artist, album_artist, album, date,
71 track, skip_until, pics=None):
72 (title, artist, album) = [(x == None and 'unknown') or x
73 for x in (title, artist, album)]
74 if date == None:
75 date = ''
76
77 if quiet:
78 flac_options = '--silent'
79 else:
80 flac_options = ''
81
82 tmp = []
83 global lame_options
84 if lame_options != None:
85 tmp.append(lame_options)
86 else:
87 tmp.append('--preset standard');
88 quiet and tmp.append('--quiet')
89 verbose and tmp.append('--verbose')
90 lame_options = ' '.join(tmp)
91
92 outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
93 track, title)).replace('/', '_')
94
95 # Escape any single quotes ' so we can quote this.
96 (fn, title, artist, album_artist,
97 album, date) = [(x or '').replace("'", r"'\''")
98 for x in [fn, title, artist, album_artist, album, date]]
99
100 album_artist_options = ''
101 if album_artist:
102 album_artist_options = "--tv 'TPE2=%s'" % album_artist
103
104 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, album,
105 track, title)).replace('/', '_')
106
107 pic_options = None
108 if pics:
109 (fd, picfn) = tempfile.mkstemp()
110 f = os.fdopen(fd, 'wb')
111 f.write(pics[0][7])
112 f.close()
113 pic_options = "--ti '%s'" % picfn
114 try:
115 run_or_die(3, "flac %s -cd %s '%s' | lame --id3v2-only --id3v2-latin1 --pad-id3v2-size 0 %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d %s %s - '%s'"
116 % (flac_options, ' '.join(skip_until), fn,
117 lame_options, title, artist, album, date, track,
118 pic_options, album_artist_options, quoted_outfile))
119 finally:
120 if pic_options:
121 try:
122 os.unlink(picfn)
123 except:
124 pass
125
126 return 0
127
128 ################################################################################
129 # The master process
130
131 def tformat(m, s, c):
132 return '%02d:%02d.%02d' % (m, s, c)
133
134 def get_decode_args(fn):
135 l = []
136
137 p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE)
138 for line in (x.rstrip() for x in p.stdout):
139 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
140 if m != None:
141 l.append(map(int, m.groups()))
142 status = p.wait()
143 # XXX dataloss! check status
144
145 args = []
146 for i in xrange(len(l)):
147 arg = ['--skip=' + tformat(*l[i])]
148 try:
149 next = l[i + 1]
150 except IndexError:
151 next = None
152 if next != None:
153 if next[2] == 0:
154 if next[1] == 0:
155 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
156 else:
157 arg.append('--until=' + tformat(next[0], next[1] - 1,
158 74))
159 else:
160 arg.append('--until=' + tformat(next[0], next[1],
161 next[2] - 1))
162
163 args.append(arg)
164
165 # If no cue sheet, stick a dummy in here.
166 if len(args) == 0:
167 args = [[]]
168
169 return args
170
171 def get_tags(fn):
172 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
173 in the file FN."""
174
175 tags = Tags()
176
177 p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE)
178 tags.load(p.stdout)
179
180 # XXX dataloss! check status
181 status = p.wait()
182
183 return tags
184
185 def main(argv):
186 # Control the exit code for any uncaught exceptions.
187 try:
188 parser = OptionParser()
189 parser.disable_interspersed_args()
190 parser.add_option('-X', '--debug', action='store_true', default=False)
191 parser.add_option('-j', '--jobs', type='int', default=1)
192 parser.add_option('--lame-options')
193 parser.add_option('-q', '--quiet', action='store_true', default=False)
194 parser.add_option('-v', '--verbose', action='store_true', default=False)
195 except:
196 traceback.print_exc()
197 return 2
198
199 try:
200 # Raises SystemExit on invalid options in argv.
201 (options, args) = parser.parse_args(argv[1:])
202 except Exception, error:
203 if isinstance(error, SystemExit):
204 return 1
205 traceback.print_exc()
206 return 2
207
208 separator = ' '
209 try:
210 global debug, flac_options, lame_options, quiet, verbose
211 debug = options.debug
212 lame_options = options.lame_options
213 quiet = options.quiet
214 verbose = options.verbose
215
216 jobs = []
217 for fn in args:
218 try:
219 args = get_decode_args(fn)
220
221 tags = get_tags(fn)
222 album = tags.gets('ALBUM', separator=separator)
223 discnum = tags.gets('DISCNUMBER')
224 track = tags.gets('TRACKNUMBER')
225
226 # lame doesn't seem to support disc number.
227 if discnum != None:
228 album = '%s (disc %s)' % (album, discnum)
229
230 # Stupid hack: only a single-track file should have the
231 # TRACKNUMBER tag, so use it if set for the first pass through
232 # the loop. At the end of the loop, we'll set $track for the
233 # next run, so this continues to work for multi-track files.
234 if track == None:
235 track = 1
236 else:
237 track = int(track)
238
239 pics = flac.get_pictures(fn)
240
241 for i in range(len(tags)):
242 title = tags.gets('TITLE', track, separator)
243 part = tags.gets('PART', track)
244 if part != None:
245 title = '%s - %s' % (title, part)
246 artist = tags.get('ARTIST', track)
247 artist.extend(tags.get('FEATURING', track))
248 album_artist = tags.gets('ALBUMARTIST', track)
249 jobs.append([fn, title,
250 ', '.join(artist),
251 album_artist, album,
252 tags.gets('DATE', track),
253 track, args[i], pics])
254 track = i + 2
255 except Exception, error:
256 sys.stderr.write(getattr(error, 'msg', ''))
257 traceback.print_exc()
258 sys.stderr.write('Continuing...\n')
259
260 def getjob(reap):
261 try:
262 job = jobs.pop(0)
263 except IndexError:
264 return
265 return lambda: flac2mp3(*job)
266 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
267 except Exception, error:
268 if isinstance(error, SystemExit):
269 raise
270 # check all print_exc and format_exc in fa-flacd.py; i think
271 # for some i don't do this msg print check
272 sys.stderr.write(getattr(error, 'msg', ''))
273 traceback.print_exc()
274 return 2
275
276 return 0
277
278 if __name__ == '__main__':
279 sys.exit(main(sys.argv))