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