]> diplodocus.org Git - flac-archive/blob - flac2mp3
Rewrite in python.
[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.util import run_or_die
59
60 ################################################################################
61 # The child processes
62
63 def flac2mp3(fn, title, artist, album, date, track, skip_until):
64 (title, artist, album, date) = [(x == None and 'unknown') or x
65 for x in (title, artist, album, date)]
66 track = int(track) # XXX caller should int this
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 %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 return 0
100
101 ################################################################################
102 # The master process
103
104 def tformat(m, s, c):
105 return '%02d:%02d.%02d' % (m, s, c)
106
107 def get_decode_args(fn):
108 l = []
109
110 p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE)
111 for line in (x.rstrip() for x in p.stdout):
112 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
113 if m != None:
114 l.append(map(int, m.groups()))
115 status = p.wait()
116 # XXX dataloss! check status
117
118 args = []
119 for i in xrange(len(l)):
120 arg = ['--skip=' + tformat(*l[i])]
121 try:
122 next = l[i + 1]
123 except IndexError:
124 next = None
125 if next != None:
126 if next[2] == 0:
127 if next[1] == 0:
128 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
129 else:
130 arg.append('--until=' + tformat(next[0], next[1] - 1,
131 74))
132 else:
133 arg.append('--until=' + tformat(next[0], next[1],
134 next[2] - 1))
135
136 args.append(arg)
137
138 # If no cue sheet, stick a dummy in here.
139 if len(args) == 0:
140 args = [[]]
141
142 return args
143
144 def get_tags(fn):
145 '''Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
146 in the file FN.'''
147
148 date = discnum = track = None
149 artists = []
150 titles = []
151
152 p = Popen(['metaflac', '--export-vc-to=-', fn], stdout=PIPE)
153 for line in (x.rstrip() for x in p.stdout):
154 (tag, value) = line.split('=', 1)
155
156 if re.match(r'ARTIST=', line, re.IGNORECASE):
157 artist = value
158 elif re.match(r'ALBUM=', line, re.IGNORECASE):
159 album = value
160 elif re.match(r'DATE=', line, re.IGNORECASE):
161 date = value
162 elif re.match(r'DISCNUMBER=', line, re.IGNORECASE):
163 discnum = value
164 elif re.match(r'TRACKNUMBER=', line, re.IGNORECASE):
165 track = value
166
167 # XXX Need to support per-track of everything but album and
168 # discnum, not just TITLE... Also unify with fa-flacd get_tags.
169 elif re.match(r'ARTIST\[', line, re.IGNORECASE):
170 artists.append(value)
171 elif re.match(r'TITLE', line, re.IGNORECASE):
172 titles.append(value)
173 # XXX dataloss! check status
174 status = p.wait()
175
176 # If no TITLEs, stick a dummy in here.
177 if len(titles) == 0:
178 titles.append(None)
179
180 return (artist, album, date, discnum, track, artists, titles)
181
182 def main(argv):
183 # Control the exit code for any uncaught exceptions.
184 try:
185 parser = OptionParser()
186 parser.disable_interspersed_args()
187 parser.add_option('-X', '--debug', action='store_true', default=False)
188 parser.add_option('-j', '--jobs', type='int', default=1)
189 parser.add_option('--lame-options')
190 parser.add_option('-q', '--quiet', action='store_true', default=False)
191 parser.add_option('-v', '--verbose', action='store_true', default=False)
192 except:
193 traceback.print_exc()
194 return 2
195
196 try:
197 # Raises SystemExit on invalid options in argv.
198 (options, args) = parser.parse_args(argv[1:])
199 except Exception, error:
200 if isinstance(error, SystemExit):
201 return 1
202 traceback.print_exc()
203 return 2
204
205 try:
206 global debug, flac_options, lame_options, quiet, verbose
207 debug = options.debug
208 lame_options = options.lame_options
209 quiet = options.quiet
210 verbose = options.verbose
211
212 jobs = []
213 for fn in args:
214 try:
215 args = get_decode_args(fn)
216 (artist, album, date, discnum, track,
217 artists, titles) = get_tags(fn)
218
219 # lame doesn't seem to support disc number.
220 if discnum != None:
221 album = '%s (disc %d)' % (album, discnum)
222
223 # Stupid hack: only a single-track file should have the
224 # TRACKNUMBER tag, so use it if set for the first pass through
225 # the loop. At the end of the loop, we'll set $track for the
226 # next run, so this continues to work for multi-track files.
227 if track == None:
228 track = 1
229
230 for i in range(len(titles)):
231 if len(artists) > 0:
232 artist = artists[i]
233 jobs.append([fn, titles[i], artist, album,
234 date, track, args[i]])
235 track = i + 2
236 except Exception, error:
237 sys.stderr.write(getattr(error, 'msg', ''))
238 traceback.print_exc()
239 sys.stderr.write('Continuing...\n')
240
241 def getjob(reap):
242 try:
243 job = jobs.pop(0)
244 except IndexError:
245 return
246 return lambda: flac2mp3(*job)
247 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
248 except Exception, error:
249 if isinstance(error, SystemExit):
250 raise
251 # check all print_exc and format_exc in fa-flacd.py; i think
252 # for some i don't do this msg print check
253 sys.stderr.write(getattr(error, 'msg', ''))
254 traceback.print_exc()
255 return 2
256
257 return 0
258
259 if __name__ == '__main__':
260 sys.exit(main(sys.argv))