]> diplodocus.org Git - flac-archive/blob - fa-rip
Write ALBUMARTIST field and adapt to some API changes.
[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, 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 try:
152 track = tracks.pop(0)
153 title = track.title
154 artist = track.artist
155 except IndexError:
156 title = ''
157 artist = ''
158 various and c(fp.write, 'ARTIST[%d]=%s\n' % (i,
159 artist.encode('utf-8')))
160 c(fp.write, 'TITLE[%d]=%s\n' % (i, title.encode('utf-8')))
161
162 c(fp.close)
163
164 def cover_art(i, asin):
165 url = 'http://images.amazon.com/images/P/%s.01.MZZZZZZZ.jpg' % (asin,)
166 fp = file('cover.front-' + i, 'w')
167 fp.write(urllib.urlopen(url).read())
168 fp.close()
169
170 def tags(q, releases, trackcount):
171 results = []
172 seen_various = False
173
174 tags_file('candidate-tags-0', trackcount, False)
175
176 include = musicbrainz2.webservice.ReleaseIncludes(artist=True, tracks=True)
177
178 i = 0
179 for rel_id in releases:
180 i += 1
181 release = q.getReleaseById(rel_id, include)
182 various = not release.isSingleArtistRelease()
183
184 if various and not seen_various:
185 seen_various = True
186 tags_file('candidate-tags-0v', trackcount, various)
187
188 tags_file('candidate-tags-' + str(i), trackcount, various,
189 release.artist.name, release.title,
190 release.getReleaseEventsAsDict(),
191 release.tracks)
192
193 # XXX Not sure if .asin is here after my change above; may need to
194 # include urlRelations=True. See also:
195 # for i in release.getRelations(): print i.type
196 # http://musicbrainz.org/ns/rel-1.0#Wikipedia
197 # ...
198 # http://musicbrainz.org/ns/rel-1.0#AmazonAsin
199 cover_art(str(i), release.asin)
200
201 def rip(device, trackcount, single_file):
202 if device == None:
203 device = '/dev/cdrom'
204
205 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
206
207 if single_file:
208 argv.append('wav')
209 else:
210 argv.append('-B')
211
212 c(os.execvp, argv[0], argv)
213
214 def make_post_processor(command):
215 if command == None:
216 return
217
218 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
219 fp = c(os.fdopen, fd, 'w')
220 c(fp.write, command +' "$@"\n')
221 c(fp.close)
222
223 def releases_by_disc(q, disc):
224 filter = musicbrainz2.webservice.ReleaseFilter(discId=disc.getId())
225 # XXX had to change x.getId to x.release.getId on 10.04; is that an API
226 # change in a newer version (handle both) or is this code I never tested?
227 return (x.release.getId() for x in q.getReleases(filter))
228
229 def releases_by(q, title, artist=None):
230 r = q.getReleases(musicbrainz2.webservice.ReleaseFilter(title=title))
231 if artist == None:
232 return r
233
234 artist = re.sub(r'\s+', r'\s+', artist.strip())
235 return (x.getId() for x in r if re.match(artist, x.release.artist.name,
236 re.IGNORECASE) != None)
237
238 def main(argv):
239 # Control the exit code for any uncaught exceptions.
240 try:
241 parser = OptionParser()
242 parser.disable_interspersed_args()
243 parser.add_option('--artist')
244 parser.add_option('--title')
245 parser.add_option('-d', '--device')
246 parser.add_option('-m', '--no-musicbrainz',
247 action='store_true', default=False)
248 parser.add_option('-p', '--post-processor')
249 parser.add_option('-s', '--single-file',
250 action='store_true', default=False)
251 parser.add_option('-t', '--tracks', type='int', default=99)
252 except:
253 traceback.print_exc()
254 return 2
255
256 try:
257 # Raises SystemExit on invalid options in argv.
258 (options, args) = parser.parse_args(argv[1:])
259 except Exception, error:
260 if isinstance(error, SystemExit):
261 return 1
262 traceback.print_exc()
263 return 2
264
265 try:
266 device = options.device
267 trackcount = options.tracks
268
269 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
270 'flac-archive.')
271 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
272 c(os.chdir, tempdir)
273
274 make_post_processor(options.post_processor)
275
276 q = musicbrainz2.webservice.Query()
277 if options.title != None:
278 releases = releases_by(q, options.title, options.artist)
279 else:
280 disc = musicbrainz2.disc.readDisc(device)
281 trackcount = mkcue(disc, trackcount)
282 if options.no_musicbrainz:
283 releases = []
284 else:
285 releases = releases_by_disc(q, disc)
286
287 tags(q, releases, trackcount)
288
289 if options.title == None:
290 rip(device, trackcount, options.single_file)
291 except Exception, error:
292 if isinstance(error, SystemExit):
293 raise
294 # check all print_exc and format_exc in fa-flacd.py; i think
295 # for some i don't do this msg print check
296 sys.stderr.write(getattr(error, 'msg', ''))
297 traceback.print_exc()
298 return 2
299
300 return 0
301
302 if __name__ == '__main__':
303 sys.exit(main(sys.argv))