]> diplodocus.org Git - flac-archive/blob - flac2mp3
split out some shared code and tidy 0-prefixing on DISCNUMBER
[flac-archive] / flac2mp3
1 #!/usr/bin/python2
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.tags import Tags
65
66 ################################################################################
67 # The child processes
68
69 def flac2mp3(fn, title, artist, album_artist, album, discnum, date,
70 track, skip_until):
71 (title, artist, album) = [(x == None and 'unknown') or x
72 for x in (title, artist, album)]
73 if date == None:
74 date = ''
75
76 if quiet:
77 flac_options = '--silent'
78 else:
79 flac_options = ''
80
81 tmp = []
82 global lame_options
83 if lame_options != None:
84 tmp.append(lame_options)
85 else:
86 tmp.append('--preset standard');
87 quiet and tmp.append('--quiet')
88 verbose and tmp.append('--verbose')
89 lame_options = ' '.join(tmp)
90
91 unquoted_fn = fn
92
93 # XXX all this quoting is an unholy mess and I can see bugs:
94 # Escaping quotes and then assembling a file name out of that?!
95 # Moved this part up 2018-11-03 and followed Microsoft file name rules.
96 outfile_album = album
97 if discnum != None:
98 outfile_album = '%s (disc %s)' % (album, discnum)
99 quoted_outfile = ('%s (%s) %02d %s.mp3' % (
100 artist, outfile_album, track, title)) \
101 .replace("'", '_') \
102 .replace('<', '_') \
103 .replace('>', '_') \
104 .replace(':', '_') \
105 .replace('"', '_') \
106 .replace('/', '_') \
107 .replace('\\', '_') \
108 .replace('|', '_') \
109 .replace('?', '_') \
110 .replace('*', '_')
111
112 # Escape any single quotes ' so we can quote this.
113 (fn, title, artist, album_artist,
114 album, date) = [(x or '').replace("'", r"'\''")
115 for x in [fn, title, artist, album_artist, album, date]]
116
117 album_artist_options = ''
118 if album_artist:
119 album_artist_options = "--tv 'TPE2=%s'" % album_artist
120
121 discnum_options = ''
122 if discnum != None:
123 discnum_options = "--tv 'TPOS=%d'" % int(discnum)
124
125 # XXX and I have no idea what this was...
126 # HACK! :(
127 if check_missing:
128 return quoted_outfile.replace(r"'\''", "'")
129
130 pic_options = ''
131 (fd, picfn) = tempfile.mkstemp()
132 os.close(fd)
133 p = Popen(['metaflac', '--export-picture-to', picfn, unquoted_fn],
134 stderr=PIPE)
135 status = p.wait()
136 stderr = ''.join(p.stderr)
137 # Hacky check for flac with no album art
138 if 'no PICTURE block' in stderr:
139 # That's fine, just no picture.
140 pass
141 else:
142 if status != 0:
143 sys.stderr.write('metaflac exited %d: %s\n' % (status, stderr))
144 return
145 pic_options = "--ti '%s'" % picfn
146 try:
147 # TODO: Look at TDOR, TDRL, TDRC for date.
148 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'"
149 % (flac_options, ' '.join(skip_until), fn,
150 lame_options, title, artist, album, date, track,
151 pic_options, album_artist_options,
152 discnum_options, quoted_outfile))
153 finally:
154 try:
155 os.unlink(picfn)
156 except:
157 pass
158
159 return 0
160
161 ################################################################################
162 # The master process
163
164 def tformat(m, s, c):
165 return '%02d:%02d.%02d' % (m, s, c)
166
167 def get_decode_args(fn):
168 l = []
169
170 p = Popen(['metaflac', '--no-utf8-convert', '--export-cuesheet-to=-', fn],
171 stdout=PIPE)
172 for line in (x.rstrip() for x in p.stdout):
173 m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line)
174 if m != None:
175 l.append(map(int, m.groups()))
176 status = p.wait()
177 # XXX dataloss! check status
178
179 args = []
180 for i in xrange(len(l)):
181 arg = ['--skip=' + tformat(*l[i])]
182 try:
183 next = l[i + 1]
184 except IndexError:
185 next = None
186 if next != None:
187 if next[2] == 0:
188 if next[1] == 0:
189 arg.append('--until=' + tformat(next[0] - 1, 59, 74))
190 else:
191 arg.append('--until=' + tformat(next[0], next[1] - 1,
192 74))
193 else:
194 arg.append('--until=' + tformat(next[0], next[1],
195 next[2] - 1))
196
197 args.append(arg)
198
199 # If no cue sheet, stick a dummy in here.
200 if len(args) == 0:
201 args = [[]]
202
203 return args
204
205 def get_tags(fn):
206 """Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags
207 in the file FN."""
208
209 tags = Tags()
210
211 p = Popen(['metaflac', '--no-utf8-convert', '--export-tags-to=-', fn],
212 stdout=PIPE)
213 tags.load(p.stdout)
214
215 # XXX dataloss! check status
216 status = p.wait()
217
218 return tags
219
220 def main(argv):
221 # Control the exit code for any uncaught exceptions.
222 try:
223 parser = OptionParser()
224 parser.disable_interspersed_args()
225 parser.add_option('-X', '--debug', action='store_true', default=False)
226 parser.add_option('-j', '--jobs', type='int', default=1)
227 parser.add_option('--lame-options')
228 parser.add_option('-q', '--quiet', action='store_true', default=False)
229 parser.add_option('-v', '--verbose', action='store_true', default=False)
230 parser.add_option('--check-missing-files', action='store_true',
231 default=False)
232 except:
233 traceback.print_exc()
234 return 2
235
236 try:
237 # Raises SystemExit on invalid options in argv.
238 (options, args) = parser.parse_args(argv[1:])
239 except Exception, error:
240 if isinstance(error, SystemExit):
241 return 1
242 traceback.print_exc()
243 return 2
244
245 separator = ' '
246 try:
247 global debug, flac_options, lame_options, quiet, verbose
248 global check_missing
249 check_missing = options.check_missing_files
250 debug = options.debug
251 lame_options = options.lame_options
252 quiet = options.quiet
253 verbose = options.verbose
254
255 jobs = []
256 for fn in args:
257 try:
258 args = get_decode_args(fn)
259
260 tags = get_tags(fn)
261 album = tags.gets('ALBUM', separator=separator)
262 discnum = tags.gets('DISCNUMBER')
263 track = tags.gets('TRACKNUMBER')
264
265 # Stupid hack: only a single-track file should have the
266 # TRACKNUMBER tag, so use it if set for the first pass through
267 # the loop. At the end of the loop, we'll set $track for the
268 # next run, so this continues to work for multi-track files.
269 if track == None:
270 track = 1
271 else:
272 track = int(track)
273
274 for i in range(len(tags)):
275 title = tags.gets('TITLE', track, separator)
276 part = tags.gets('PART', track)
277 if part != None:
278 title = '%s - %s' % (title, part)
279 version = tags.gets('VERSION', track)
280 if version != None:
281 title = '%s (%s)' % (title, version)
282 artist = tags.get('ARTIST', track)
283 artist.extend(tags.get('FEATURING', track))
284 album_artist = tags.gets('ALBUMARTIST', track)
285 if check_missing:
286 mp3 = flac2mp3(fn, title,
287 ', '.join(artist),
288 album_artist, album, discnum,
289 tags.gets('DATE', track),
290 track, args[i])
291 if not os.path.exists(mp3):
292 print fn
293 break
294 continue
295 jobs.append((fn, title,
296 ', '.join(artist),
297 album_artist, album, discnum,
298 tags.gets('DATE', track),
299 track, args[i]))
300 track = i + 2
301 except Exception, error:
302 sys.stderr.write(getattr(error, 'msg', ''))
303 traceback.print_exc()
304 sys.stderr.write('Continuing...\n')
305
306 def getjob(reap):
307 try:
308 job = jobs.pop(0)
309 except IndexError:
310 return
311 return lambda: flac2mp3(*job)
312 org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob)
313 except Exception, error:
314 if isinstance(error, SystemExit):
315 raise
316 # check all print_exc and format_exc in fa-flacd.py; i think
317 # for some i don't do this msg print check
318 sys.stderr.write(getattr(error, 'msg', ''))
319 traceback.print_exc()
320 return 2
321
322 return 0
323
324 if __name__ == '__main__':
325 sys.exit(main(sys.argv))