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