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