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