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