]> diplodocus.org Git - flac-archive/blob - fa-rip
Don't make so many calls to Musicbrainz web service.
[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
191 # XXX Not sure if .asin is here after my change above; may need to
192 # include urlRelations=True. See also:
193 # for i in release.getRelations(): print i.type
194 # http://musicbrainz.org/ns/rel-1.0#Wikipedia
195 # ...
196 # http://musicbrainz.org/ns/rel-1.0#AmazonAsin
197 cover_art(str(i), release.asin)
198
199 def rip(device, trackcount, single_file):
200 if device == None:
201 device = '/dev/cdrom'
202
203 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
204
205 if single_file:
206 argv.append('wav')
207 else:
208 argv.append('-B')
209
210 c(os.execvp, argv[0], argv)
211
212 def make_post_processor(command):
213 if command == None:
214 return
215
216 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
217 fp = c(os.fdopen, fd, 'w')
218 c(fp.write, command +' "$@"\n')
219 c(fp.close)
220
221 def get_releases(filter_, tries=5):
222 sleep = 1
223 query = musicbrainz2.webservice.Query()
224 while True:
225 try:
226 return query.getReleases(filter_)
227 except musicbrainz2.webservice.WebServiceError, e:
228 if '503' not in e.msg:
229 raise
230 tries -= 1
231 sys.stderr.write('getReleases: %s: ' % e)
232 if tries == 0:
233 sys.stderr.write('giving up\n')
234 raise
235 sleep *= 2
236 sys.stderr.write('sleeping %ds before retry...\n' % sleep)
237 time.sleep(sleep)
238
239 def releases_by_disc(disc_id):
240 filter_ = musicbrainz2.webservice.ReleaseFilter(discId=disc_id)
241 return (result.release for result in get_releases(filter_))
242
243 def releases_by(q, title, artist=None):
244 filter_ = musicbrainz2.webservice.ReleaseFilter(title=title)
245 results = get_releases(filter_)
246 releases = (result.release for result in results)
247 if artist:
248 pattern = re.sub(r'\s+', r'\s+', artist.strip())
249 releases = (x for x in releases
250 if re.match(pattern, x.artist.name, re.IGNORECASE))
251 return releases
252
253 def main(argv):
254 # Control the exit code for any uncaught exceptions.
255 try:
256 parser = OptionParser()
257 parser.disable_interspersed_args()
258 parser.add_option('--artist')
259 parser.add_option('--discid')
260 parser.add_option('--title')
261 parser.add_option('-d', '--device')
262 parser.add_option('-m', '--no-musicbrainz',
263 action='store_true', default=False)
264 parser.add_option('-p', '--post-processor')
265 parser.add_option('-s', '--single-file',
266 action='store_true', default=False)
267 parser.add_option('-t', '--tracks', type='int', default=99)
268 except:
269 traceback.print_exc()
270 return 2
271
272 try:
273 # Raises SystemExit on invalid options in argv.
274 (options, args) = parser.parse_args(argv[1:])
275 except Exception, error:
276 if isinstance(error, SystemExit):
277 return 1
278 traceback.print_exc()
279 return 2
280
281 try:
282 device = options.device
283 trackcount = options.tracks
284
285 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
286 'flac-archive.')
287 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
288 c(os.chdir, tempdir)
289
290 make_post_processor(options.post_processor)
291
292 if options.title != None:
293 tags(releases_by(q, options.title, options.artist), trackcount)
294 elif options.discid != None:
295 tags(releases_by_disc(options.discid), trackcount)
296 else:
297 disc = musicbrainz2.disc.readDisc(device)
298 trackcount = mkcue(disc, trackcount)
299 if options.no_musicbrainz:
300 releases = []
301 else:
302 releases = releases_by_disc(disc.getId())
303 tags(releases, trackcount)
304 rip(device, trackcount, options.single_file)
305 except Exception, error:
306 if isinstance(error, SystemExit):
307 raise
308 # check all print_exc and format_exc in fa-flacd.py; i think
309 # for some i don't do this msg print check
310 sys.stderr.write(getattr(error, 'msg', ''))
311 traceback.print_exc()
312 return 2
313
314 return 0
315
316 if __name__ == '__main__':
317 sys.exit(main(sys.argv))