]> diplodocus.org Git - flac-archive/blob - fa-rip
ac35baed59841846a512fa750ecac13fa4db2b1f
[flac-archive] / fa-rip
1 #!/usr/bin/python
2
3 """
4 =head1 NAME
5
6 B<fa-rip> - rip a CD for B<fa-flacd>
7
8 =head1 SYNOPSIS
9
10 B<fa-rip> [B<--artist> I<artist> B<--title> I<title>] [B<-d> I<device>] [B<-m>] [B<-p> I<post-processor>] [B<-s>] [B<-t> I<track-count>]
11
12 =head1 DESCRIPTION
13
14 B<fa-rip> creates a temporary directory for storage of its
15 intermediate files, uses MusicBrainz to create the "cue" file and
16 candidate tags files, and runs C<cdparanoia(1)> to rip the CD to the
17 "wav" file.
18
19 In order for this CD to be processed by B<fa-flacd>, you must create a
20 "tags" file. This is usually done by renaming one of the
21 candidate-tags files and deleting the others. Don't forget to fill in
22 the DATE tag in the selected candidate before renaming it. If
23 B<fa-rip> could not find any tag information from MusicBrainz, you'll
24 have to fill out the candidate-tags-0 template.
25
26 =head1 OPTIONS
27
28 =over 4
29
30 =item B<--artist> I<artist> B<--title> I<title>
31
32 Write candidate-tags files based on I<artist> and album I<title>.
33 Useful if you've already ripped wav files with some other program and
34 just need to set things up for B<fa-flacd>.
35
36 =item B<-d> [B<--device>] I<device>
37
38 Use I<device> as the CD-ROM device, instead of the default
39 "/dev/cdrom" or the environment variable CDDEV.
40
41 =item B<-m> [B<--no-musicbrainz>]
42
43 Don't connect to MusicBrainz, just write candidate-tags-0.
44
45 =item B<-p> [B<--post-processor>] I<post-processor>
46
47 Create a "post-processor" file in the temporary directory containing
48 the line 'I<post-processor> "$@"'. See B<fa-flacd>'s man page for
49 information about this hook.
50
51 =item B<-s> [B<--single-file>]
52
53 Rip whole disc to one wav file and configure B<fa-flacd> to encode it
54 to one FLAC file with embedded cuesheet.
55
56 =item B<-t> [B<--tracks>] I<track-count>
57
58 Archive only the first I<track-count> tracks. This is handy for
59 ignoring data tracks.
60
61 =back
62
63 =head1 ENVIRONMENT
64
65 =over 4
66
67 =item CDDEV
68
69 B<fa-rip> uses this to rip audio and save the cuesheet for a CD.
70 MusicBrainz::Client can usually figure this out automatically.
71
72 =back
73
74 =head1 AUTHORS
75
76 Written by Eric Gillespie <epg@pretzelnet.org>.
77
78 flac-archive is free software; you may redistribute it and/or modify
79 it under the same terms as Perl itself.
80
81 =cut
82
83 """
84
85 import os, re, sys, tempfile, time, traceback
86 from optparse import OptionParser
87 import urllib
88
89 import musicbrainz2.disc
90 import musicbrainz2.webservice
91
92 from org.diplodocus.util import catch_EnvironmentError as c
93
94 # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=439790
95 MSF_OFFSET = 150
96
97 def mkcue(disc, trackcount=None):
98 fp = c(file, 'cue', 'w')
99 c(fp.write, 'FILE "dummy.wav" WAVE\n')
100
101 if trackcount == None:
102 trackcount = disc.lastTrackNum
103 else:
104 trackcount = min(trackcount, disc.lastTrackNum)
105
106 for i in xrange(disc.firstTrackNum, trackcount+1):
107 offset = disc.tracks[i-1][0]
108 offset -= MSF_OFFSET
109
110 minutes = seconds = 0
111 sectors = offset % 75
112 if offset >= 75:
113 seconds = offset / 75
114 if seconds >= 60:
115 minutes = seconds / 60
116 seconds = seconds % 60
117
118 c(fp.write, ' TRACK %02d AUDIO\n' % (i,))
119 if i == 1 and offset > 0:
120 c(fp.write, ' INDEX 00 00:00:00\n')
121 c(fp.write,
122 ' INDEX 01 %02d:%02d:%02d\n' % (minutes, seconds, sectors))
123
124 c(fp.close)
125
126 return trackcount
127
128 def tags_file(fn, trackcount, various, artist=None, album=None,
129 release_dates={}, tracks=[]):
130 fp = c(file, fn, 'w')
131 if various:
132 c(fp.write, 'ALBUMARTIST=')
133 else:
134 c(fp.write, 'ARTIST=')
135 if artist != None:
136 c(fp.write, artist.encode('utf-8'))
137 c(fp.write, '\nALBUM=')
138 if album != None:
139 c(fp.write, album.encode('utf-8'))
140 c(fp.write, '\n')
141
142 have_date = False
143 for (country, date) in release_dates.items():
144 have_date = True
145 c(fp.write, 'DATE[%s]=%s\n' % (country, date))
146 have_date or c(fp.write, 'DATE=\n')
147
148 if len(tracks) > 0:
149 trackcount = min(trackcount, len(tracks))
150 for i in xrange(1, trackcount + 1):
151 artist = title = ''
152 if len(tracks) > 0:
153 track = tracks.pop(0)
154 title = track.title
155 if track.artist:
156 artist = track.artist.name
157 various and c(fp.write, 'ARTIST[%d]=%s\n' % (i,
158 artist.encode('utf-8')))
159 c(fp.write, 'TITLE[%d]=%s\n' % (i, title.encode('utf-8')))
160
161 c(fp.close)
162
163 def cover_art(i, asin):
164 url = 'http://images.amazon.com/images/P/%s.01.MZZZZZZZ.jpg' % (asin,)
165 fp = file('cover.front-' + i, 'w')
166 fp.write(urllib.urlopen(url).read())
167 fp.close()
168
169 def tags(releases, trackcount):
170 results = []
171 seen_various = False
172
173 tags_file('candidate-tags-0', trackcount, False)
174
175 include = musicbrainz2.webservice.ReleaseIncludes(artist=True, tracks=True)
176
177 i = 0
178 for release in releases:
179 i += 1
180 various = not release.isSingleArtistRelease()
181
182 if various and not seen_various:
183 seen_various = True
184 tags_file('candidate-tags-0v', trackcount, various)
185
186 tags_file('candidate-tags-' + str(i), trackcount, various,
187 release.artist.name, release.title,
188 release.getReleaseEventsAsDict(),
189 release.tracks)
190 if release.asin:
191 # See also:
192 # for i in release.getRelations(): print i.type
193 # http://musicbrainz.org/ns/rel-1.0#Wikipedia
194 # ...
195 # http://musicbrainz.org/ns/rel-1.0#AmazonAsin
196 cover_art(str(i), release.asin)
197
198 def rip(device, trackcount, single_file):
199 if device == None:
200 device = '/dev/cdrom'
201
202 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
203
204 if single_file:
205 argv.append('wav')
206 else:
207 argv.append('-B')
208
209 c(os.execvp, argv[0], argv)
210
211 def make_post_processor(command):
212 if command == None:
213 return
214
215 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
216 fp = c(os.fdopen, fd, 'w')
217 c(fp.write, command +' "$@"\n')
218 c(fp.close)
219
220 def get_releases(filter_, tries=5):
221 sleep = 1
222 query = musicbrainz2.webservice.Query()
223 while True:
224 try:
225 return query.getReleases(filter_)
226 except musicbrainz2.webservice.WebServiceError, e:
227 if '503' not in e.msg:
228 raise
229 tries -= 1
230 sys.stderr.write('getReleases: %s: ' % e)
231 if tries == 0:
232 sys.stderr.write('giving up\n')
233 raise
234 sleep *= 2
235 sys.stderr.write('sleeping %ds before retry...\n' % sleep)
236 time.sleep(sleep)
237
238 def releases_by_disc(disc_id):
239 filter_ = musicbrainz2.webservice.ReleaseFilter(discId=disc_id)
240 return (result.release for result in get_releases(filter_))
241
242 def releases_by(q, title, artist=None):
243 filter_ = musicbrainz2.webservice.ReleaseFilter(title=title)
244 results = get_releases(filter_)
245 releases = (result.release for result in results)
246 if artist:
247 pattern = re.sub(r'\s+', r'\s+', artist.strip())
248 releases = (x for x in releases
249 if re.match(pattern, x.artist.name, re.IGNORECASE))
250 return releases
251
252 def main(argv):
253 # Control the exit code for any uncaught exceptions.
254 try:
255 parser = OptionParser()
256 parser.disable_interspersed_args()
257 parser.add_option('--artist')
258 parser.add_option('--discid')
259 parser.add_option('--title')
260 parser.add_option('-d', '--device')
261 parser.add_option('-m', '--no-musicbrainz',
262 action='store_true', default=False)
263 parser.add_option('-p', '--post-processor')
264 parser.add_option('-s', '--single-file',
265 action='store_true', default=False)
266 parser.add_option('-t', '--tracks', type='int', default=99)
267 except:
268 traceback.print_exc()
269 return 2
270
271 try:
272 # Raises SystemExit on invalid options in argv.
273 (options, args) = parser.parse_args(argv[1:])
274 except Exception, error:
275 if isinstance(error, SystemExit):
276 return 1
277 traceback.print_exc()
278 return 2
279
280 try:
281 device = options.device
282 trackcount = options.tracks
283
284 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
285 'flac-archive.')
286 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
287 c(os.chdir, tempdir)
288
289 make_post_processor(options.post_processor)
290
291 if options.title != None:
292 tags(releases_by(q, options.title, options.artist), trackcount)
293 elif options.discid != None:
294 tags(releases_by_disc(options.discid), trackcount)
295 else:
296 disc = musicbrainz2.disc.readDisc(device)
297 trackcount = mkcue(disc, trackcount)
298 if options.no_musicbrainz:
299 releases = []
300 else:
301 releases = releases_by_disc(disc.getId())
302 tags(releases, trackcount)
303 rip(device, trackcount, options.single_file)
304 except Exception, error:
305 if isinstance(error, SystemExit):
306 raise
307 # check all print_exc and format_exc in fa-flacd.py; i think
308 # for some i don't do this msg print check
309 sys.stderr.write(getattr(error, 'msg', ''))
310 traceback.print_exc()
311 return 2
312
313 return 0
314
315 if __name__ == '__main__':
316 sys.exit(main(sys.argv))