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