]> diplodocus.org Git - flac-archive/blob - fa-rip
6a087644180cc4c4d357c110db6d90caaeafe26b
[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<-d> I<device>] [B<-p> I<post-processor> [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<-d> [B<--device>] I<device>
31
32 Use I<device> as the CD-ROM device, instead of the default
33 "/dev/cdrom" or the environment variable CDDEV.
34
35 =item B<-p> [B<--post-processor>] I<post-processor>
36
37 Create a "post-processor" file in the temporary directory containing
38 the line 'I<post-processor> "$@"'. See B<fa-flacd>'s man page for
39 information about this hook.
40
41 =item B<-t> [B<--tracks>] I<track-count>
42
43 Archive only the first I<track-count> tracks. This is handy for
44 ignoring data tracks.
45
46 =back
47
48 =head1 ENVIRONMENT
49
50 =over 4
51
52 =item CDDEV
53
54 B<fa-rip> uses this to rip audio and save the cuesheet for a CD.
55 MusicBrainz::Client can usually figure this out automatically.
56
57 =back
58
59 =head1 AUTHORS
60
61 Written by Eric Gillespie <epg@pretzelnet.org>.
62
63 flac-archive is free software; you may redistribute it and/or modify
64 it under the same terms as Perl itself.
65
66 =cut
67
68 """
69
70 import os, re, sys, tempfile, traceback
71 from optparse import OptionParser
72 import urllib
73
74 import musicbrainz2.disc
75 import musicbrainz2.webservice
76
77 from org.diplodocus.util import catch_EnvironmentError as c
78
79 # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=439790
80 MSF_OFFSET = 150
81
82 def mkcue(disc, trackcount=None):
83 fp = c(file, 'cue', 'w')
84 c(fp.write, 'FILE "dummy.wav" WAVE\n')
85
86 if trackcount == None:
87 trackcount = disc.lastTrackNum
88 else:
89 trackcount = min(trackcount, disc.lastTrackNum)
90
91 for i in xrange(disc.firstTrackNum, trackcount+1):
92 offset = disc.tracks[i-1][0]
93 offset -= MSF_OFFSET
94
95 minutes = seconds = 0
96 sectors = offset % 75
97 if offset >= 75:
98 seconds = offset / 75
99 if seconds >= 60:
100 minutes = seconds / 60
101 seconds = seconds % 60
102
103 c(fp.write, ' TRACK %02d AUDIO\n' % (i,))
104 if i == 1 and offset > 0:
105 c(fp.write, ' INDEX 00 00:00:00\n')
106 c(fp.write,
107 ' INDEX 01 %02d:%02d:%02d\n' % (minutes, seconds, sectors))
108
109 c(fp.close)
110
111 return trackcount
112
113 def tags_file(fn, trackcount, various, artist=None, album=None,
114 release_dates={}, tracks=[]):
115 fp = c(file, fn, 'w')
116 c(fp.write, 'ARTIST=')
117 if artist != None:
118 c(fp.write, artist.encode('utf-8'))
119 c(fp.write, '\nALBUM=')
120 if album != None:
121 c(fp.write, album.encode('utf-8'))
122 c(fp.write, '\n')
123
124 have_date = False
125 for (country, date) in release_dates.items():
126 have_date = True
127 c(fp.write, 'DATE[%s]=%s\n' % (country, date))
128 have_date or c(fp.write, 'DATE=\n')
129
130 if len(tracks) > 0:
131 trackcount = min(trackcount, len(tracks))
132 for i in xrange(1, trackcount + 1):
133 try:
134 track = tracks.pop(0)
135 title = track.title
136 artist = track.artist
137 except IndexError:
138 title = ''
139 artist = ''
140 various and c(fp.write, 'ARTIST[%d]=%s\n' % (i,
141 artist.encode('utf-8')))
142 c(fp.write, 'TITLE[%d]=%s\n' % (i, title.encode('utf-8')))
143
144 c(fp.close)
145
146 def cover_art(i, asin):
147 url = 'http://images.amazon.com/images/P/%s.01.MZZZZZZZ.jpg' % (asin,)
148 fp = file('cover.front-' + i, 'w')
149 fp.write(urllib.urlopen(url).read())
150 fp.close()
151
152 def tags(q, releases, trackcount):
153 results = []
154 seen_various = False
155
156 tags_file('candidate-tags-0', trackcount, False)
157
158 include = musicbrainz2.webservice.ReleaseIncludes(tracks=True)
159
160 i = 0
161 for album in releases:
162 i += 1
163 various = not album.release.isSingleArtistRelease()
164
165 if various and not seen_various:
166 seen_various = True
167 tags_file('candidate-tags-0v', trackcount, True)
168
169 tags_file('candidate-tags-' + str(i), trackcount, various,
170 album.release.artist.name, album.release.title,
171 album.release.getReleaseEventsAsDict(),
172 q.getReleaseById(album.release.id, include).tracks)
173
174 cover_art(str(i), album.release.asin)
175
176 def rip(device, trackcount, single_file):
177 if device == None:
178 device = '/dev/cdrom'
179
180 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
181
182 if single_file:
183 argv.append('wav')
184 else:
185 argv.append('-B')
186
187 c(os.execvp, argv[0], argv)
188
189 def make_post_processor(command):
190 if command == None:
191 return
192
193 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
194 fp = c(os.fdopen, fd, 'w')
195 c(fp.write, command +' "$@"\n')
196 c(fp.close)
197
198 def releases_by_disc(q, disc):
199 filter = musicbrainz2.webservice.ReleaseFilter(discId=disc.getId())
200 return q.getReleases(filter)
201
202 def releases_by(q, title, artist=None):
203 r = q.getReleases(musicbrainz2.webservice.ReleaseFilter(title=title))
204 if artist == None:
205 return r
206
207 artist = re.sub(r'\s+', r'\s+', artist.strip())
208 return [x for x in r if re.match(artist, x.release.artist.name,
209 re.IGNORECASE) != None]
210
211 def main(argv):
212 # Control the exit code for any uncaught exceptions.
213 try:
214 parser = OptionParser()
215 parser.disable_interspersed_args()
216 parser.add_option('--artist')
217 parser.add_option('--title')
218 parser.add_option('-d', '--device')
219 parser.add_option('-m', '--no-musicbrainz',
220 action='store_true', default=False)
221 parser.add_option('-p', '--post-processor')
222 parser.add_option('-s', '--single-file',
223 action='store_true', default=False)
224 parser.add_option('-t', '--tracks', type='int', default=99)
225 except:
226 traceback.print_exc()
227 return 2
228
229 try:
230 # Raises SystemExit on invalid options in argv.
231 (options, args) = parser.parse_args(argv[1:])
232 except Exception, error:
233 if isinstance(error, SystemExit):
234 return 1
235 traceback.print_exc()
236 return 2
237
238 try:
239 device = options.device
240 trackcount = options.tracks
241
242 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
243 'flac-archive.')
244 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
245 c(os.chdir, tempdir)
246
247 make_post_processor(options.post_processor)
248
249 q = musicbrainz2.webservice.Query()
250 if options.title != None:
251 releases = releases_by(q, options.title, options.artist)
252 else:
253 disc = musicbrainz2.disc.readDisc(device)
254 trackcount = mkcue(disc, trackcount)
255 if options.no_musicbrainz:
256 releases = []
257 else:
258 releases = releases_by_disc(q, disc)
259
260 tags(q, releases, trackcount)
261
262 if options.title == None:
263 rip(device, trackcount, options.single_file)
264 except Exception, error:
265 if isinstance(error, SystemExit):
266 raise
267 # check all print_exc and format_exc in fa-flacd.py; i think
268 # for some i don't do this msg print check
269 sys.stderr.write(getattr(error, 'msg', ''))
270 traceback.print_exc()
271 return 2
272
273 return 0
274
275 if __name__ == '__main__':
276 sys.exit(main(sys.argv))