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