]> diplodocus.org Git - flac-archive/blob - flac2mp3
Use TPOS to store discnumber in mp3 files.
[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 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 outfile_album = album
105 discnum_options = ''
106 if discnum != None:
107 outfile_album = '%s (disc %s)' % (album, discnum)
108 discnum_options = "--tv 'TPOS=%d'" % int(discnum)
109
110 quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, outfile_album,
111 track, title)).replace('/', '_')
112
113 pic_options = None
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 except:
205 traceback.print_exc()
206 return 2
207
208 try:
209 # Raises SystemExit on invalid options in argv.
210 (options, args) = parser.parse_args(argv[1:])
211 except Exception, error:
212 if isinstance(error, SystemExit):
213 return 1
214 traceback.print_exc()
215 return 2
216
217 separator = ' '
218 try:
219 global debug, flac_options, lame_options, quiet, verbose
220 debug = options.debug
221 lame_options = options.lame_options
222 quiet = options.quiet
223 verbose = options.verbose
224
225 jobs = []
226 for fn in args:
227 try:
228 args = get_decode_args(fn)
229
230 tags = get_tags(fn)
231 album = tags.gets('ALBUM', separator=separator)
232 discnum = tags.gets('DISCNUMBER')
233 track = tags.gets('TRACKNUMBER')
234
235 # Stupid hack: only a single-track file should have the
236 # TRACKNUMBER tag, so use it if set for the first pass through
237 # the loop. At the end of the loop, we'll set $track for the
238 # next run, so this continues to work for multi-track files.
239 if track == None:
240 track = 1
241 else:
242 track = int(track)
243
244 pics = flac.get_pictures(fn)
245
246 for i in range(len(tags)):
247 title = tags.gets('TITLE', track, separator)
248 part = tags.gets('PART', track)
249 if part != None:
250 title = '%s - %s' % (title, part)
251 version = tags.gets('VERSION', track)
252 if version != None:
253 title = '%s (%s)' % (title, version)
254 artist = tags.get('ARTIST', track)
255 artist.extend(tags.get('FEATURING', track))
256 album_artist = tags.gets('ALBUMARTIST', track)
257 jobs.append((fn, title,
258 ', '.join(artist),
259 album_artist, album, discnum,
260 tags.gets('DATE', track),
261 track, args[i], pics))
262 track = i + 2
263 except Exception, error:
264 sys.stderr.write(getattr(error, 'msg', ''))
265 traceback.print_exc()
266 sys.stderr.write('Continuing...\n')
267
268 def getjob(reap):
269 try:
270 job = jobs.pop(0)
271 except IndexError:
272 return
273 return lambda: flac2mp3(*job)
274 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
275 except Exception, error:
276 if isinstance(error, SystemExit):
277 raise
278 # check all print_exc and format_exc in fa-flacd.py; i think
279 # for some i don't do this msg print check
280 sys.stderr.write(getattr(error, 'msg', ''))
281 traceback.print_exc()
282 return 2
283
284 return 0
285
286 if __name__ == '__main__':
287 sys.exit(main(sys.argv))