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