]> diplodocus.org Git - flac-archive/blob - flac2mp3
note how stupid lame is
[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 picfn = 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 try:
114 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 --ti '%s' %s - '%s'"
115 % (flac_options, ' '.join(skip_until), fn,
116 lame_options, title, artist, album, date, track,
117 picfn, album_artist_options, quoted_outfile))
118 finally:
119 if picfn:
120 try:
121 os.unlink(picfn)
122 except:
123 pass
124
125 return 0
126
127 ################################################################################
128 # The master process
129
130 def tformat(m, s, c):
131 return '%02d:%02d.%02d' % (m, s, c)
132
133 def get_decode_args(fn):
134 l = []
135
136 p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE)
137 for line in (x.rstrip() for x in p.stdout):
138 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
139 if m != None:
140 l.append(map(int, m.groups()))
141 status = p.wait()
142 # XXX dataloss! check status
143
144 args = []
145 for i in xrange(len(l)):
146 arg = ['--skip=' + tformat(*l[i])]
147 try:
148 next = l[i + 1]
149 except IndexError:
150 next = None
151 if next != None:
152 if next[2] == 0:
153 if next[1] == 0:
154 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
155 else:
156 arg.append('--until=' + tformat(next[0], next[1] - 1,
157 74))
158 else:
159 arg.append('--until=' + tformat(next[0], next[1],
160 next[2] - 1))
161
162 args.append(arg)
163
164 # If no cue sheet, stick a dummy in here.
165 if len(args) == 0:
166 args = [[]]
167
168 return args
169
170 def get_tags(fn):
171 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
172 in the file FN."""
173
174 tags = Tags()
175
176 p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE)
177 tags.load(p.stdout)
178
179 # XXX dataloss! check status
180 status = p.wait()
181
182 return tags
183
184 def main(argv):
185 # Control the exit code for any uncaught exceptions.
186 try:
187 parser = OptionParser()
188 parser.disable_interspersed_args()
189 parser.add_option('-X', '--debug', action='store_true', default=False)
190 parser.add_option('-j', '--jobs', type='int', default=1)
191 parser.add_option('--lame-options')
192 parser.add_option('-q', '--quiet', action='store_true', default=False)
193 parser.add_option('-v', '--verbose', action='store_true', default=False)
194 except:
195 traceback.print_exc()
196 return 2
197
198 try:
199 # Raises SystemExit on invalid options in argv.
200 (options, args) = parser.parse_args(argv[1:])
201 except Exception, error:
202 if isinstance(error, SystemExit):
203 return 1
204 traceback.print_exc()
205 return 2
206
207 separator = ' '
208 try:
209 global debug, flac_options, lame_options, quiet, verbose
210 debug = options.debug
211 lame_options = options.lame_options
212 quiet = options.quiet
213 verbose = options.verbose
214
215 jobs = []
216 for fn in args:
217 try:
218 args = get_decode_args(fn)
219
220 tags = get_tags(fn)
221 album = tags.gets('ALBUM', separator=separator)
222 discnum = tags.gets('DISCNUMBER')
223 track = tags.gets('TRACKNUMBER')
224
225 # lame doesn't seem to support disc number.
226 if discnum != None:
227 album = '%s (disc %s)' % (album, discnum)
228
229 # Stupid hack: only a single-track file should have the
230 # TRACKNUMBER tag, so use it if set for the first pass through
231 # the loop. At the end of the loop, we'll set $track for the
232 # next run, so this continues to work for multi-track files.
233 if track == None:
234 track = 1
235 else:
236 track = int(track)
237
238 pics = flac.get_pictures(fn)
239
240 for i in range(len(tags)):
241 title = tags.gets('TITLE', track, separator)
242 part = tags.gets('PART', track)
243 if part != None:
244 title = '%s - %s' % (title, part)
245 artist = tags.get('ARTIST', track)
246 artist.extend(tags.get('FEATURING', track))
247 album_artist = tags.gets('ALBUMARTIST', track)
248 jobs.append([fn, title,
249 ', '.join(artist),
250 album_artist, album,
251 tags.gets('DATE', track),
252 track, args[i], pics])
253 track = i + 2
254 except Exception, error:
255 sys.stderr.write(getattr(error, 'msg', ''))
256 traceback.print_exc()
257 sys.stderr.write('Continuing...\n')
258
259 def getjob(reap):
260 try:
261 job = jobs.pop(0)
262 except IndexError:
263 return
264 return lambda: flac2mp3(*job)
265 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
266 except Exception, error:
267 if isinstance(error, SystemExit):
268 raise
269 # check all print_exc and format_exc in fa-flacd.py; i think
270 # for some i don't do this msg print check
271 sys.stderr.write(getattr(error, 'msg', ''))
272 traceback.print_exc()
273 return 2
274
275 return 0
276
277 if __name__ == '__main__':
278 sys.exit(main(sys.argv))