source: trunk/morituri/rip/cd.py @ 420

Revision 420, 12.5 KB checked in by thomas, 2 years ago (diff)
  • morituri/common/program.py:
  • morituri/image/table.py:
  • morituri/rip/cd.py: Get CDDB disc id. Use it to print info when not found on MusicBrainz?.
Line 
1# -*- Mode: Python -*-
2# vi:si:et:sw=4:sts=4:ts=4
3
4# Morituri - for those about to RIP
5
6# Copyright (C) 2009 Thomas Vander Stichele
7
8# This file is part of morituri.
9#
10# morituri is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# morituri is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with morituri.  If not, see <http://www.gnu.org/licenses/>.
22
23import os
24import math
25
26import gobject
27gobject.threads_init()
28
29from morituri.common import logcommand, task, common, accurip, log
30from morituri.common import drive, program
31from morituri.result import result
32from morituri.image import image, cue, table
33from morituri.program import cdrdao, cdparanoia
34
35
36class Rip(logcommand.LogCommand):
37    summary = "rip CD"
38
39    description = """
40Rips a CD.
41
42Tracks are named according to the track template:
43 - %t: track number
44 - %a: track artist
45 - %n: track title
46 - %s: track sort name
47
48Discs are named according to the disc template:
49 - %A: album artist
50 - %S: album sort name
51 - %d: disc title
52"""
53
54    def addOptions(self):
55        # FIXME: get from config
56        default = 0
57        self.parser.add_option('-o', '--offset',
58            action="store", dest="offset",
59            help="sample read offset (defaults to %d)" % default,
60            default=default)
61        self.parser.add_option('-O', '--output-directory',
62            action="store", dest="output_directory",
63            help="output directory (defaults to current directory)")
64        # FIXME: have a cache of these pickles somewhere
65        self.parser.add_option('-T', '--toc-pickle',
66            action="store", dest="toc_pickle",
67            help="pickle to use for reading and writing the TOC",
68            default=default)
69        # FIXME: get from config
70        default = '%A - %d/%t. %a - %n'
71        self.parser.add_option('', '--track-template',
72            action="store", dest="track_template",
73            help="template for track file naming (default %s)" % default,
74            default=default)
75        default = '%A - %d/%A - %d'
76        self.parser.add_option('', '--disc-template',
77            action="store", dest="disc_template",
78            help="template for disc file naming (default %s)" % default,
79            default=default)
80        default = 'flac'
81
82        # here to avoid import gst eating our options
83        from morituri.common import encode
84
85        self.parser.add_option('', '--profile',
86            action="store", dest="profile",
87            help="profile for encoding (default '%s', choices '%s')" % (
88                default, "', '".join(encode.PROFILES.keys())),
89            default=default)
90        self.parser.add_option('-U', '--unknown',
91            action="store_true", dest="unknown",
92            help="whether to continue ripping if the CD is unknown (%default)",
93            default=False)
94        default = 'flac'
95
96
97    def handleOptions(self, options):
98        options.track_template = options.track_template.decode('utf-8')
99        options.disc_template = options.disc_template.decode('utf-8')
100
101    def do(self, args):
102        prog = program.Program()
103        runner = task.SyncRunner()
104
105        def function(r, t):
106            r.run(t)
107
108        # if the device is mounted (data session), unmount it
109        device = self.parentCommand.options.device
110        print 'Checking device', device
111
112        prog.loadDevice(device)
113        prog.unmountDevice(device)
114       
115        # first, read the normal TOC, which is fast
116        ptoc = common.Persister(self.options.toc_pickle or None)
117        if not ptoc.object:
118            t = cdrdao.ReadTOCTask(device=device)
119            function(runner, t)
120            version = t.tasks[1].parser.version
121            from pkg_resources import parse_version as V
122            # we've built a cdrdao 1.2.3rc2 modified package with the patch
123            if V(version) < V('1.2.3rc2p1'):
124                print '''
125Warning: cdrdao older than 1.2.3 has a pre-gap length bug.
126See  http://sourceforge.net/tracker/?func=detail&aid=604751&group_id=2171&atid=102171
127'''
128            ptoc.persist(t.table)
129        ittoc = ptoc.object
130        assert ittoc.hasTOC()
131
132        # already show us some info based on this
133        prog.getRipResult(ittoc.getCDDBDiscId())
134        print "CDDB disc id", ittoc.getCDDBDiscId()
135        mbdiscid = ittoc.getMusicBrainzDiscId()
136        print "MusicBrainz disc id", mbdiscid
137
138        prog.metadata = prog.getMusicBrainz(ittoc, mbdiscid)
139
140        if not prog.metadata:
141            # fall back to FreeDB for lookup
142            cddbid = ittoc.getCDDBValues()
143            cddbmd = prog.getCDDB(cddbid)
144            if cddbmd:
145                print 'FreeDB identifies disc as %s' % cddbmd
146
147            if not self.options.unknown:
148                prog.ejectDevice(device)
149                return -1
150
151        # now, read the complete index table, which is slower
152        itable = prog.getTable(runner, ittoc.getCDDBDiscId(), device)
153
154        assert itable.getCDDBDiscId() == ittoc.getCDDBDiscId(), \
155            "full table's id %s differs from toc id %s" % (
156                itable.getCDDBDiscId(), ittoc.getCDDBDiscId())
157        assert itable.getMusicBrainzDiscId() == ittoc.getMusicBrainzDiscId(), \
158            "full table's mb id %s differs from toc id mb %s" % (
159            itable.getMusicBrainzDiscId(), ittoc.getMusicBrainzDiscId())
160        assert itable.getAccurateRipURL() == ittoc.getAccurateRipURL(), \
161            "full table's AR URL %s differs from toc AR URL %s" % (
162            itable.getAccurateRipURL(), ittoc.getAccurateRipURL())
163
164        prog.outdir = (self.options.output_directory or os.getcwd())
165        prog.outdir = prog.outdir.decode('utf-8')
166        # here to avoid import gst eating our options
167        from morituri.common import encode
168        profile = encode.PROFILES[self.options.profile]()
169
170        # result
171
172        prog.result.offset = int(self.options.offset)
173        prog.result.artist = prog.metadata and prog.metadata.artist or 'Unknown Artist'
174        prog.result.title = prog.metadata and prog.metadata.title or 'Unknown Title'
175        # cdio is optional for now
176        try:
177            import cdio
178            _, prog.result.vendor, prog.result.model, __ = cdio.Device(device).get_hwinfo()
179        except ImportError:
180            print 'WARNING: pycdio not installed, cannot identify drive'
181            prog.result.vendor = 'Unknown'
182            prog.result.model = 'Unknown'
183
184        # FIXME: turn this into a method
185        def ripIfNotRipped(number):
186            # we can have a previous result
187            trackResult = prog.result.getTrackResult(number)
188            if not trackResult:
189                trackResult = result.TrackResult()
190                prog.result.tracks.append(trackResult)
191
192            path = prog.getPath(prog.outdir, self.options.track_template, 
193                mbdiscid, number) + '.' + profile.extension
194            trackResult.number = number
195           
196            assert type(path) is unicode, "%r is not unicode" % path
197            trackResult.filename = path
198            if number > 0:
199                trackResult.pregap = itable.tracks[number - 1].getPregap()
200
201            # FIXME: optionally allow overriding reripping
202            if os.path.exists(path):
203                print 'Verifying track %d of %d: %s' % (
204                    number, len(itable.tracks),
205                    os.path.basename(path).encode('utf-8'))
206                if not prog.verifyTrack(runner, trackResult):
207                    print 'Verification failed, reripping...'
208                    os.unlink(path)
209
210            if not os.path.exists(path):
211                print 'Ripping track %d of %d: %s' % (
212                    number, len(itable.tracks),
213                    os.path.basename(path).encode('utf-8'))
214                prog.ripTrack(runner, trackResult, 
215                    offset=int(self.options.offset),
216                    device=self.parentCommand.options.device,
217                    profile=profile,
218                    taglist=prog.getTagList(number))
219
220                if trackResult.testcrc == trackResult.copycrc:
221                    print 'Checksums match for track %d' % (number)
222                else:
223                    print 'ERROR: checksums did not match for track %d' % (
224                        number)
225                print 'Peak level: %.2f %%' % (math.sqrt(trackResult.peak) * 100.0, )
226                print 'Rip quality: %.2f %%' % (trackResult.quality * 100.0, )
227
228            # overlay this rip onto the Table
229            if number == 0:
230                # HTOA goes on index 0 of track 1
231                itable.setFile(1, 0, path, ittoc.getTrackStart(1),
232                    number)
233            else:
234                itable.setFile(number, 1, path, ittoc.getTrackLength(number),
235                    number)
236
237            prog.saveRipResult()
238
239
240        # check for hidden track one audio
241        htoapath = None
242        htoa = prog.getHTOA()
243        if htoa:
244            start, stop = htoa
245            print 'Found Hidden Track One Audio from frame %d to %d' % (
246                start, stop)
247               
248            # rip it
249            ripIfNotRipped(0)
250            htoapath = prog.result.tracks[0].filename
251
252        for i, track in enumerate(itable.tracks):
253            # FIXME: rip data tracks differently
254            if not track.audio:
255                print 'WARNING: skipping data track %d, not implemented' % (
256                    i + 1, )
257                # FIXME: make it work for now
258                track.indexes[1].relative = 0
259                continue
260
261            ripIfNotRipped(i + 1)
262
263        ### write disc files
264        discName = prog.getPath(prog.outdir, self.options.disc_template, 
265            mbdiscid, 0)
266        dirname = os.path.dirname(discName)
267        if not os.path.exists(dirname):
268            os.makedirs(dirname)
269
270        self.debug('writing cue file for %r', discName)
271        prog.writeCue(discName)
272
273        # write .m3u file
274        m3uPath = u'%s.m3u' % discName
275        handle = open(m3uPath, 'w')
276        handle.write(u'#EXTM3U\n')
277        if htoapath:
278            u = u'#EXTINF:%d,%s\n' % (
279                itable.getTrackStart(1) / common.FRAMES_PER_SECOND,
280                    os.path.basename(htoapath[:-4]))
281            handle.write(u.encode('utf-8'))
282            u = '%s\n' % os.path.basename(htoapath)
283            handle.write(u.encode('utf-8'))
284
285        for i, track in enumerate(itable.tracks):
286            if not track.audio:
287                continue
288
289            path = prog.getPath(prog.outdir, self.options.track_template, 
290                mbdiscid, i + 1) + '.' + profile.extension
291            u = u'#EXTINF:%d,%s\n' % (
292                itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND,
293                os.path.basename(path))
294            handle.write(u.encode('utf-8'))
295            u = '%s\n' % os.path.basename(path)
296            handle.write(u.encode('utf-8'))
297        handle.close()
298
299        # verify using accuraterip
300        url = ittoc.getAccurateRipURL()
301        print "AccurateRip URL", url
302
303        cache = accurip.AccuCache()
304        responses = cache.retrieve(url)
305
306        if not responses:
307            print 'Album not found in AccurateRip database'
308
309        if responses:
310            print '%d AccurateRip reponses found' % len(responses)
311
312            if responses[0].cddbDiscId != itable.getCDDBDiscId():
313                print "AccurateRip response discid different: %s" % \
314                    responses[0].cddbDiscId
315
316           
317        prog.verifyImage(runner, responses)
318
319        print "\n".join(prog.getAccurateRipResults()) + "\n"
320
321        # write log file
322        logger = result.getLogger()
323        prog.writeLog(discName, logger)
324
325        prog.ejectDevice(device)
326
327
328class CD(logcommand.LogCommand):
329    summary = "handle CD's"
330
331    subCommandClasses = [Rip, ]
332
333    def addOptions(self):
334        self.parser.add_option('-d', '--device',
335            action="store", dest="device",
336            help="CD-DA device")
337 
338    def handleOptions(self, options):
339        if not options.device:
340            drives = drive.getAllDevicePaths()
341            if not drives:
342                self.error('No CD-DA drives found!')
343                return 3
344       
345            # pick the first
346            self.options.device = drives[0]
347
348        # this can be a symlink to another device
349        self.options.device = os.path.realpath(self.options.device)
Note: See TracBrowser for help on using the repository browser.