]> diplodocus.org Git - flac-archive/blob - fa-rip
Simplify (initial structure was minimal transformation of the Python)
[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, time, traceback
86 from optparse import OptionParser
87 import urllib
88
89 import discid.disc
90 import musicbrainzngs.musicbrainz
91
92 from org.diplodocus.util import catch_EnvironmentError as c
93
94 musicbrainzngs.musicbrainz.set_useragent(
95 'flac-archive', '0.1', 'https://diplodocus.org/git/flac-archive')
96
97 #import logging
98 #logging.basicConfig(level=logging.DEBUG)
99
100 # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=439790
101 MSF_OFFSET = 150
102
103 def mkcue(fp, disc, trackcount=None):
104 c(fp.write, 'FILE "dummy.wav" WAVE\n')
105
106 if trackcount == None:
107 trackcount = len(disc.tracks)
108 else:
109 trackcount = min(trackcount, len(disc.tracks))
110
111 for i in xrange(1, trackcount+1):
112 track = disc.tracks[i-1]
113 offset = track.offset - MSF_OFFSET
114
115 minutes = seconds = 0
116 sectors = offset % 75
117 if offset >= 75:
118 seconds = offset / 75
119 if seconds >= 60:
120 minutes = seconds / 60
121 seconds = seconds % 60
122
123 c(fp.write, ' TRACK %02d AUDIO\n' % (track.number,))
124 if i == 1 and offset > 0:
125 c(fp.write, ' INDEX 00 00:00:00\n')
126 c(fp.write,
127 ' INDEX 01 %02d:%02d:%02d\n' % (minutes, seconds, sectors))
128
129 return trackcount
130
131 def tags_file(fn, trackcount, various, artist=None, album=None,
132 release_dates={}, tracks=[]):
133 fp = c(file, fn, 'w')
134 if various:
135 c(fp.write, 'ALBUMARTIST=')
136 else:
137 c(fp.write, 'ARTIST=')
138 if artist != None:
139 c(fp.write, artist.encode('utf-8'))
140 c(fp.write, '\nALBUM=')
141 if album != None:
142 c(fp.write, album.encode('utf-8'))
143 c(fp.write, '\n')
144
145 have_date = False
146 for (country, date) in release_dates.items():
147 have_date = True
148 c(fp.write, 'DATE[%s]=%s\n' % (country, date))
149 have_date or c(fp.write, 'DATE=\n')
150
151 if len(tracks) > 0:
152 trackcount = min(trackcount, len(tracks))
153 for i in xrange(1, trackcount + 1):
154 artist = title = ''
155 if len(tracks) > 0:
156 track = tracks.pop(0)
157 title = track.title
158 if track.artist:
159 artist = track.artist.name
160 various and c(fp.write, 'ARTIST[%d]=%s\n' % (i,
161 artist.encode('utf-8')))
162 c(fp.write, 'TITLE[%d]=%s\n' % (i, title.encode('utf-8')))
163
164 c(fp.close)
165
166 def cover_art(i, asin):
167 url = 'http://images.amazon.com/images/P/%s.01.MZZZZZZZ.jpg' % (asin,)
168 fp = file('cover.front-' + i, 'w')
169 fp.write(urllib.urlopen(url).read())
170 fp.close()
171
172 def tags(releases, trackcount):
173 results = []
174 seen_asins = set()
175 seen_various = False
176
177 tags_file('candidate-tags-0', trackcount, False)
178
179 i = 0
180 for release in releases:
181 i += 1
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 if release.asin:
193 # See also:
194 # for i in release.getRelations(): print i.type
195 # http://musicbrainz.org/ns/rel-1.0#Wikipedia
196 # ...
197 # http://musicbrainz.org/ns/rel-1.0#AmazonAsin
198 asin = release.asin
199 if asin not in seen_asins:
200 seen_asins.add(asin)
201 cover_art(str(i), asin)
202
203 def rip(device, trackcount, single_file):
204 if device == None:
205 device = '/dev/cdrom'
206
207 argv = ['cdparanoia', '-d', device, '1-' + str(trackcount)]
208
209 if single_file:
210 argv.append('wav')
211 else:
212 argv.append('-B')
213
214 c(os.execvp, argv[0], argv)
215
216 def make_post_processor(command):
217 if command == None:
218 return
219
220 fd = c(os.open, 'post-processor', os.O_CREAT | os.O_WRONLY, 0555)
221 fp = c(os.fdopen, fd, 'w')
222 c(fp.write, command +' "$@"\n')
223 c(fp.close)
224
225 def get_releases(filter_, tries=5):
226 sleep = 1
227 query = musicbrainz2.webservice.Query()
228 while True:
229 try:
230 return query.getReleases(filter_)
231 except musicbrainz2.webservice.WebServiceError, e:
232 if '503' not in e.msg:
233 raise
234 tries -= 1
235 sys.stderr.write('getReleases: %s: ' % e)
236 if tries == 0:
237 sys.stderr.write('giving up\n')
238 raise
239 sleep *= 2
240 sys.stderr.write('sleeping %ds before retry...\n' % sleep)
241 time.sleep(sleep)
242
243 def releases_by_disc(disc_id):
244 try:
245 musicbrainzngs.musicbrainz.get_releases_by_discid(disc_id)
246 except musicbrainzngs.musicbrainz.ResponseError:
247 return []
248 raise 'what now'
249
250 def releases_by(q, title, artist=None):
251 filter_ = musicbrainz2.webservice.ReleaseFilter(title=title)
252 results = get_releases(filter_)
253 releases = (result.release for result in results)
254 if artist:
255 pattern = re.sub(r'\s+', r'\s+', artist.strip())
256 releases = (x for x in releases
257 if re.match(pattern, x.artist.name, re.IGNORECASE))
258 return releases
259
260 def main(argv):
261 # Control the exit code for any uncaught exceptions.
262 try:
263 parser = OptionParser()
264 parser.disable_interspersed_args()
265 parser.add_option('--artist')
266 parser.add_option('--discid')
267 parser.add_option('--title')
268 parser.add_option('--print-discid', action='store_true', default=False)
269 parser.add_option('-d', '--device')
270 parser.add_option('-m', '--no-musicbrainz',
271 action='store_true', default=False)
272 parser.add_option('-p', '--post-processor')
273 parser.add_option('-s', '--single-file',
274 action='store_true', default=False)
275 parser.add_option('-t', '--tracks', type='int', default=99)
276 except:
277 traceback.print_exc()
278 return 2
279
280 try:
281 # Raises SystemExit on invalid options in argv.
282 (options, args) = parser.parse_args(argv[1:])
283 except Exception, error:
284 if isinstance(error, SystemExit):
285 return 1
286 traceback.print_exc()
287 return 2
288
289 try:
290 device = options.device
291 trackcount = options.tracks
292
293 tempdir = c((lambda x: tempfile.mkdtemp(prefix=x, dir='.')),
294 'flac-archive.')
295 sys.stderr.write('ripping to %s\n\n' % (tempdir,))
296 c(os.chdir, tempdir)
297
298 make_post_processor(options.post_processor)
299
300 if options.title != None:
301 tags(releases_by(q, options.title, options.artist), trackcount)
302 elif options.discid != None:
303 tags(releases_by_disc(options.discid), trackcount)
304 else:
305 disc = discid.disc.read(device)
306 if options.print_discid:
307 print disc.id
308 return 0
309 fp = c(file, 'cue', 'w')
310 trackcount = mkcue(fp, disc, trackcount)
311 c(fp.close)
312 if options.no_musicbrainz:
313 releases = []
314 else:
315 releases = releases_by_disc(disc.id)
316 tags(releases, trackcount)
317 rip(device, trackcount, options.single_file)
318 except Exception, error:
319 if isinstance(error, SystemExit):
320 raise
321 # check all print_exc and format_exc in fa-flacd.py; i think
322 # for some i don't do this msg print check
323 sys.stderr.write(getattr(error, 'msg', ''))
324 traceback.print_exc()
325 return 2
326
327 return 0
328
329 if __name__ == '__main__':
330 sys.exit(main(sys.argv))