]> diplodocus.org Git - flac-archive/blob - flac2mp3
Hmm, some standardization sitting around in a working copy since November...
[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, discnum, 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 # Escape any single quotes ' so we can quote this.
93 (fn, title, artist, album_artist,
94 album, date) = [(x or '').replace("'", r"'\''")
95 for x in [fn, title, artist, album_artist, album, date]]
96
97 album_artist_options = ''
98 if album_artist:
99 album_artist_options = "--tv 'TPE2=%s'" % album_artist
100
101 outfile_album = album
102 discnum_options = ''
103 if discnum != None:
104 outfile_album = '%s (disc %s)' % (album, discnum)
105 discnum_options = "--tv 'TPOS=%d'" % int(discnum)
106
107 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, outfile_album,
108 track, title)).replace('/', '_')
109
110 pic_options = None
111 if pics:
112 (fd, picfn) = tempfile.mkstemp()
113 f = os.fdopen(fd, 'wb')
114 f.write(pics[0][7])
115 f.close()
116 pic_options = "--ti '%s'" % picfn
117 try:
118 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'"
119 % (flac_options, ' '.join(skip_until), fn,
120 lame_options, title, artist, album, date, track,
121 pic_options, album_artist_options,
122 discnum_options, quoted_outfile))
123 finally:
124 if pic_options:
125 try:
126 os.unlink(picfn)
127 except:
128 pass
129
130 return 0
131
132 ################################################################################
133 # The master process
134
135 def tformat(m, s, c):
136 return '%02d:%02d.%02d' % (m, s, c)
137
138 def get_decode_args(fn):
139 l = []
140
141 p = Popen(['metaflac', '--no-utf8-convert', '--export-cuesheet-to=-', fn],
142 stdout=PIPE)
143 for line in (x.rstrip() for x in p.stdout):
144 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
145 if m != None:
146 l.append(map(int, m.groups()))
147 status = p.wait()
148 # XXX dataloss! check status
149
150 args = []
151 for i in xrange(len(l)):
152 arg = ['--skip=' + tformat(*l[i])]
153 try:
154 next = l[i + 1]
155 except IndexError:
156 next = None
157 if next != None:
158 if next[2] == 0:
159 if next[1] == 0:
160 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
161 else:
162 arg.append('--until=' + tformat(next[0], next[1] - 1,
163 74))
164 else:
165 arg.append('--until=' + tformat(next[0], next[1],
166 next[2] - 1))
167
168 args.append(arg)
169
170 # If no cue sheet, stick a dummy in here.
171 if len(args) == 0:
172 args = [[]]
173
174 return args
175
176 def get_tags(fn):
177 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
178 in the file FN."""
179
180 tags = Tags()
181
182 p = Popen(['metaflac', '--no-utf8-convert', '--export-tags-to=-', fn],
183 stdout=PIPE)
184 tags.load(p.stdout)
185
186 # XXX dataloss! check status
187 status = p.wait()
188
189 return tags
190
191 def main(argv):
192 # Control the exit code for any uncaught exceptions.
193 try:
194 parser = OptionParser()
195 parser.disable_interspersed_args()
196 parser.add_option('-X', '--debug', action='store_true', default=False)
197 parser.add_option('-j', '--jobs', type='int', default=1)
198 parser.add_option('--lame-options')
199 parser.add_option('-q', '--quiet', action='store_true', default=False)
200 parser.add_option('-v', '--verbose', action='store_true', default=False)
201 except:
202 traceback.print_exc()
203 return 2
204
205 try:
206 # Raises SystemExit on invalid options in argv.
207 (options, args) = parser.parse_args(argv[1:])
208 except Exception, error:
209 if isinstance(error, SystemExit):
210 return 1
211 traceback.print_exc()
212 return 2
213
214 separator = ' '
215 try:
216 global debug, flac_options, lame_options, quiet, verbose
217 debug = options.debug
218 lame_options = options.lame_options
219 quiet = options.quiet
220 verbose = options.verbose
221
222 jobs = []
223 for fn in args:
224 try:
225 args = get_decode_args(fn)
226
227 tags = get_tags(fn)
228 album = tags.gets('ALBUM', separator=separator)
229 discnum = tags.gets('DISCNUMBER')
230 track = tags.gets('TRACKNUMBER')
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, discnum,
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))