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