]> diplodocus.org Git - flac-archive/blob - flac2mp3
Commit a bunch of state sitting around in the old bzr checkout!
[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.tags import Tags
65
66 ################################################################################
67 # The child processes
68
69 def flac2mp3(fn, title, artist, album_artist, album, discnum, date,
70 track, skip_until):
71 (title, artist, album) = [(x == None and 'unknown') or x
72 for x in (title, artist, album)]
73 if date == None:
74 date = ''
75
76 if quiet:
77 flac_options = '--silent'
78 else:
79 flac_options = ''
80
81 tmp = []
82 global lame_options
83 if lame_options != None:
84 tmp.append(lame_options)
85 else:
86 tmp.append('--preset standard');
87 quiet and tmp.append('--quiet')
88 verbose and tmp.append('--verbose')
89 lame_options = ' '.join(tmp)
90
91 unquoted_fn = fn
92
93 # Escape any single quotes ' so we can quote this.
94 (fn, title, artist, album_artist,
95 album, date) = [(x or '').replace("'", r"'\''")
96 for x in [fn, title, artist, album_artist, album, date]]
97
98 album_artist_options = ''
99 if album_artist:
100 album_artist_options = "--tv 'TPE2=%s'" % album_artist
101
102 outfile_album = album
103 discnum_options = ''
104 if discnum != None:
105 outfile_album = '%s (disc %s)' % (album, discnum)
106 discnum_options = "--tv 'TPOS=%d'" % int(discnum)
107
108 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, outfile_album,
109 track, title)).replace('/', '_')
110 # HACK! :(
111 if check_missing:
112 return quoted_outfile.replace(r"'\''", "'")
113
114 pic_options = ''
115 (fd, picfn) = tempfile.mkstemp()
116 os.close(fd)
117 p = Popen(['metaflac', '--export-picture-to', picfn, unquoted_fn],
118 stderr=PIPE)
119 status = p.wait()
120 stderr = ''.join(p.stderr)
121 # Hacky check for flac with no album art
122 if 'no PICTURE block' in stderr:
123 # That's fine, just no picture.
124 pass
125 else:
126 if status != 0:
127 sys.stderr.write('metaflac exited %d: %s\n' % (status, stderr))
128 return
129 pic_options = "--ti '%s'" % picfn
130 try:
131 # TODO: Look at TDOR, TDRL, TDRC for date.
132 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 - '%s'"
133 % (flac_options, ' '.join(skip_until), fn,
134 lame_options, title, artist, album, date, track,
135 pic_options, album_artist_options,
136 discnum_options, quoted_outfile))
137 finally:
138 try:
139 os.unlink(picfn)
140 except:
141 pass
142
143 return 0
144
145 ################################################################################
146 # The master process
147
148 def tformat(m, s, c):
149 return '%02d:%02d.%02d' % (m, s, c)
150
151 def get_decode_args(fn):
152 l = []
153
154 p = Popen(['metaflac', '--no-utf8-convert', '--export-cuesheet-to=-', fn],
155 stdout=PIPE)
156 for line in (x.rstrip() for x in p.stdout):
157 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
158 if m != None:
159 l.append(map(int, m.groups()))
160 status = p.wait()
161 # XXX dataloss! check status
162
163 args = []
164 for i in xrange(len(l)):
165 arg = ['--skip=' + tformat(*l[i])]
166 try:
167 next = l[i + 1]
168 except IndexError:
169 next = None
170 if next != None:
171 if next[2] == 0:
172 if next[1] == 0:
173 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
174 else:
175 arg.append('--until=' + tformat(next[0], next[1] - 1,
176 74))
177 else:
178 arg.append('--until=' + tformat(next[0], next[1],
179 next[2] - 1))
180
181 args.append(arg)
182
183 # If no cue sheet, stick a dummy in here.
184 if len(args) == 0:
185 args = [[]]
186
187 return args
188
189 def get_tags(fn):
190 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
191 in the file FN."""
192
193 tags = Tags()
194
195 p = Popen(['metaflac', '--no-utf8-convert', '--export-tags-to=-', fn],
196 stdout=PIPE)
197 tags.load(p.stdout)
198
199 # XXX dataloss! check status
200 status = p.wait()
201
202 return tags
203
204 def main(argv):
205 # Control the exit code for any uncaught exceptions.
206 try:
207 parser = OptionParser()
208 parser.disable_interspersed_args()
209 parser.add_option('-X', '--debug', action='store_true', default=False)
210 parser.add_option('-j', '--jobs', type='int', default=1)
211 parser.add_option('--lame-options')
212 parser.add_option('-q', '--quiet', action='store_true', default=False)
213 parser.add_option('-v', '--verbose', action='store_true', default=False)
214 parser.add_option('--check-missing-files', action='store_true',
215 default=False)
216 except:
217 traceback.print_exc()
218 return 2
219
220 try:
221 # Raises SystemExit on invalid options in argv.
222 (options, args) = parser.parse_args(argv[1:])
223 except Exception, error:
224 if isinstance(error, SystemExit):
225 return 1
226 traceback.print_exc()
227 return 2
228
229 separator = ' '
230 try:
231 global debug, flac_options, lame_options, quiet, verbose
232 global check_missing
233 check_missing = options.check_missing_files
234 debug = options.debug
235 lame_options = options.lame_options
236 quiet = options.quiet
237 verbose = options.verbose
238
239 jobs = []
240 for fn in args:
241 try:
242 args = get_decode_args(fn)
243
244 tags = get_tags(fn)
245 album = tags.gets('ALBUM', separator=separator)
246 discnum = tags.gets('DISCNUMBER')
247 track = tags.gets('TRACKNUMBER')
248
249 # Stupid hack: only a single-track file should have the
250 # TRACKNUMBER tag, so use it if set for the first pass through
251 # the loop. At the end of the loop, we'll set $track for the
252 # next run, so this continues to work for multi-track files.
253 if track == None:
254 track = 1
255 else:
256 track = int(track)
257
258 for i in range(len(tags)):
259 title = tags.gets('TITLE', track, separator)
260 part = tags.gets('PART', track)
261 if part != None:
262 title = '%s - %s' % (title, part)
263 version = tags.gets('VERSION', track)
264 if version != None:
265 title = '%s (%s)' % (title, version)
266 artist = tags.get('ARTIST', track)
267 artist.extend(tags.get('FEATURING', track))
268 album_artist = tags.gets('ALBUMARTIST', track)
269 if check_missing:
270 mp3 = flac2mp3(fn, title,
271 ', '.join(artist),
272 album_artist, album, discnum,
273 tags.gets('DATE', track),
274 track, args[i])
275 if not os.path.exists(mp3):
276 print fn
277 break
278 continue
279 jobs.append((fn, title,
280 ', '.join(artist),
281 album_artist, album, discnum,
282 tags.gets('DATE', track),
283 track, args[i]))
284 track = i + 2
285 except Exception, error:
286 sys.stderr.write(getattr(error, 'msg', ''))
287 traceback.print_exc()
288 sys.stderr.write('Continuing...\n')
289
290 def getjob(reap):
291 try:
292 job = jobs.pop(0)
293 except IndexError:
294 return
295 return lambda: flac2mp3(*job)
296 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
297 except Exception, error:
298 if isinstance(error, SystemExit):
299 raise
300 # check all print_exc and format_exc in fa-flacd.py; i think
301 # for some i don't do this msg print check
302 sys.stderr.write(getattr(error, 'msg', ''))
303 traceback.print_exc()
304 return 2
305
306 return 0
307
308 if __name__ == '__main__':
309 sys.exit(main(sys.argv))