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