]> diplodocus.org Git - flac-archive/blob - fa-rip
Finalize release notes.
[flac-archive] / fa-rip
1 #! /usr/bin/env python2.4
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 c(fp.write, 'ARTIST=')
132 if artist != None:
133 c(fp.write, artist.encode('utf-8'))
134 c(fp.write, '\nALBUM=')
135 if album != None:
136 c(fp.write, album.encode('utf-8'))
137 c(fp.write, '\n')
138
139 have_date = False
140 for (country, date) in release_dates.items():
141 have_date = True
142 c(fp.write, 'DATE[%s]=%s\n' % (country, date))
143 have_date or c(fp.write, 'DATE=\n')
144
145 if len(tracks) > 0:
146 trackcount = min(trackcount, len(tracks))
147 for i in xrange(1, trackcount + 1):
148 try:
149 track = tracks.pop(0)
150 title = track.title
151 artist = track.artist
152 except IndexError:
153 title = ''
154 artist = ''
155 various and c(fp.write, 'ARTIST[%d]=%s\n' % (i,
156 artist.encode('utf-8')))
157 c(fp.write, 'TITLE[%d]=%s\n' % (i, title.encode('utf-8')))
158
159 c(fp.close)
160
161 def cover_art(i, asin):
162 url = 'http://images.amazon.com/images/P/%s.01.MZZZZZZZ.jpg' % (asin,)
163 fp = file('cover.front-' + i, 'w')
164 fp.write(urllib.urlopen(url).read())
165 fp.close()
166
167 def tags(q, releases, trackcount):
168 results = []
169 seen_various = False
170
171 tags_file('candidate-tags-0', trackcount, False)
172
173 include = musicbrainz2.webservice.ReleaseIncludes(tracks=True)
174
175 i = 0
176 for album in releases:
177 i += 1
178 various = not album.release.isSingleArtistRelease()
179
180 if various and not seen_various:
181 seen_various = True
182 tags_file('candidate-tags-0v', trackcount, True)
183
184 tags_file('candidate-tags-' + str(i), trackcount, various,
185 album.release.artist.name, album.release.title,
186 album.release.getReleaseEventsAsDict(),
187 q.getReleaseById(album.release.id, include).tracks)
188
189 cover_art(str(i), album.release.asin)
190
191 def rip(device, trackcount, single_file):
192 if device == None:
193 device = '/dev/cdrom'
194
195 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
196
197 if single_file:
198 argv.append('wav')
199 else:
200 argv.append('-B')
201
202 c(os.execvp, argv[0], argv)
203
204 def make_post_processor(command):
205 if command == None:
206 return
207
208 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
209 fp = c(os.fdopen, fd, 'w')
210 c(fp.write, command +' "$@"\n')
211 c(fp.close)
212
213 def releases_by_disc(q, disc):
214 filter = musicbrainz2.webservice.ReleaseFilter(discId=disc.getId())
215 return q.getReleases(filter)
216
217 def releases_by(q, title, artist=None):
218 r = q.getReleases(musicbrainz2.webservice.ReleaseFilter(title=title))
219 if artist == None:
220 return r
221
222 artist = re.sub(r'\s+', r'\s+', artist.strip())
223 return [x for x in r if re.match(artist, x.release.artist.name,
224 re.IGNORECASE) != None]
225
226 def main(argv):
227 # Control the exit code for any uncaught exceptions.
228 try:
229 parser = OptionParser()
230 parser.disable_interspersed_args()
231 parser.add_option('--artist')
232 parser.add_option('--title')
233 parser.add_option('-d', '--device')
234 parser.add_option('-m', '--no-musicbrainz',
235 action='store_true', default=False)
236 parser.add_option('-p', '--post-processor')
237 parser.add_option('-s', '--single-file',
238 action='store_true', default=False)
239 parser.add_option('-t', '--tracks', type='int', default=99)
240 except:
241 traceback.print_exc()
242 return 2
243
244 try:
245 # Raises SystemExit on invalid options in argv.
246 (options, args) = parser.parse_args(argv[1:])
247 except Exception, error:
248 if isinstance(error, SystemExit):
249 return 1
250 traceback.print_exc()
251 return 2
252
253 try:
254 device = options.device
255 trackcount = options.tracks
256
257 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
258 'flac-archive.')
259 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
260 c(os.chdir, tempdir)
261
262 make_post_processor(options.post_processor)
263
264 q = musicbrainz2.webservice.Query()
265 if options.title != None:
266 releases = releases_by(q, options.title, options.artist)
267 else:
268 disc = musicbrainz2.disc.readDisc(device)
269 trackcount = mkcue(disc, trackcount)
270 if options.no_musicbrainz:
271 releases = []
272 else:
273 releases = releases_by_disc(q, disc)
274
275 tags(q, releases, trackcount)
276
277 if options.title == None:
278 rip(device, trackcount, options.single_file)
279 except Exception, error:
280 if isinstance(error, SystemExit):
281 raise
282 # check all print_exc and format_exc in fa-flacd.py; i think
283 # for some i don't do this msg print check
284 sys.stderr.write(getattr(error, 'msg', ''))
285 traceback.print_exc()
286 return 2
287
288 return 0
289
290 if __name__ == '__main__':
291 sys.exit(main(sys.argv))