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