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