From 7c3118e1f2d5257e779c79fb74b157ead64580de Mon Sep 17 00:00:00 2001 From: Thomas Vander Stichele Date: Sat, 2 Feb 2013 17:06:16 +0100 Subject: [PATCH] feature: add 'rip cd info' split up CD command into a base class and two subclasses. Make instance variables accordingly. --- morituri/rip/cd.py | 351 ++++++++++++++++++++++++--------------------- 1 file changed, 190 insertions(+), 161 deletions(-) diff --git a/morituri/rip/cd.py b/morituri/rip/cd.py index ebe8283..ebcc09e 100644 --- a/morituri/rip/cd.py +++ b/morituri/rip/cd.py @@ -38,7 +38,138 @@ from morituri.extern.task import task MAX_TRIES = 5 -class Rip(logcommand.LogCommand): +class _CD(logcommand.LogCommand): + + def addOptions(self): + # FIXME: have a cache of these pickles somewhere + self.parser.add_option('-T', '--toc-pickle', + action="store", dest="toc_pickle", + help="pickle to use for reading and writing the TOC") + self.parser.add_option('-R', '--release-id', + action="store", dest="release_id", + help="MusicBrainz release id to match to (if there are multiple)") + + + def do(self, args): + self.program = program.Program(record=self.getRootCommand().record, + stdout=self.stdout) + self.runner = task.SyncRunner() + + def function(r, t): + r.run(t) + + # if the device is mounted (data session), unmount it + self.device = self.parentCommand.options.device + self.stdout.write('Checking device %s\n' % self.device) + + self.program.loadDevice(self.device) + self.program.unmountDevice(self.device) + + version = None + + # first, read the normal TOC, which is fast + ptoc = cache.Persister(self.options.toc_pickle or None) + if not ptoc.object: + tries = 0 + while True: + tries += 1 + t = cdrdao.ReadTOCTask(device=self.device) + try: + function(self.runner, t) + break + except: + if tries > 3: + raise + self.debug('failed to read TOC after %d tries, retrying' % tries) + + version = t.tasks[1].parser.version + from pkg_resources import parse_version as V + # we've built a cdrdao 1.2.3rc2 modified package with the patch + if V(version) < V('1.2.3rc2p1'): + self.stdout.write('Warning: cdrdao older than 1.2.3 has a ' + 'pre-gap length bug.\n' + 'See http://sourceforge.net/tracker/?func=detail' + '&aid=604751&group_id=2171&atid=102171\n') + ptoc.persist(t.table) + self.ittoc = ptoc.object + assert self.ittoc.hasTOC() + + # already show us some info based on this + self.program.getRipResult(self.ittoc.getCDDBDiscId()) + self.stdout.write("CDDB disc id: %s\n" % self.ittoc.getCDDBDiscId()) + self.mbdiscid = self.ittoc.getMusicBrainzDiscId() + self.stdout.write("MusicBrainz disc id %s\n" % self.mbdiscid) + + self.stdout.write("MusicBrainz lookup URL %s\n" % + self.ittoc.getMusicBrainzSubmitURL()) + + self.program.metadata = self.program.getMusicBrainz(self.ittoc, self.mbdiscid, + release=self.options.release_id) + + if not self.program.metadata: + # fall back to FreeDB for lookup + cddbid = self.ittoc.getCDDBValues() + cddbmd = self.program.getCDDB(cddbid) + if cddbmd: + self.stdout.write('FreeDB identifies disc as %s\n' % cddbmd) + + if not self.options.unknown: + self.program.ejectDevice(self.device) + return -1 + + # now, read the complete index table, which is slower + self.itable = self.program.getTable(self.runner, self.ittoc.getCDDBDiscId(), self.device) + + assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \ + "full table's id %s differs from toc id %s" % ( + self.itable.getCDDBDiscId(), self.ittoc.getCDDBDiscId()) + assert self.itable.getMusicBrainzDiscId() == self.ittoc.getMusicBrainzDiscId(), \ + "full table's mb id %s differs from toc id mb %s" % ( + self.itable.getMusicBrainzDiscId(), self.ittoc.getMusicBrainzDiscId()) + assert self.itable.getAccurateRipURL() == self.ittoc.getAccurateRipURL(), \ + "full table's AR URL %s differs from toc AR URL %s" % ( + self.itable.getAccurateRipURL(), self.ittoc.getAccurateRipURL()) + + # result + + self.program.result.cdrdaoVersion = version + self.program.result.cdparanoiaVersion = cdparanoia.getCdParanoiaVersion() + info = drive.getDeviceInfo(self.parentCommand.options.device) + if info: + try: + self.program.result.cdparanoiaDefeatsCache = self.getRootCommand( + ).config.getDefeatsCache(*info) + except KeyError, e: + self.debug('Got key error: %r' % (e, )) + self.program.result.artist = self.program.metadata and self.program.metadata.artist \ + or 'Unknown Artist' + self.program.result.title = self.program.metadata and self.program.metadata.title \ + or 'Unknown Title' + # cdio is optional for now + try: + import cdio + _, self.program.result.vendor, self.program.result.model, self.program.result.release = \ + cdio.Device(self.device).get_hwinfo() + except ImportError: + self.stdout.write( + 'WARNING: pycdio not installed, cannot identify drive\n') + self.program.result.vendor = 'Unknown' + self.program.result.model = 'Unknown' + self.program.result.release = 'Unknown' + + self.doCommand() + + self.program.ejectDevice(self.device) + + def doCommand(self): + pass + + +class Info(_CD): + pass + + +class Rip(_CD): summary = "rip CD" # see morituri.common.program.Program.getPath for expansion @@ -55,6 +186,8 @@ Log files will log the path to tracks relative to this directory. """ % rcommon.TEMPLATE_DESCRIPTION def addOptions(self): + _CD.addOptions(self) + loggers = result.getLoggers().keys() self.parser.add_option('-L', '--logger', @@ -77,17 +210,9 @@ Log files will log the path to tracks relative to this directory. action="store", dest="working_directory", help="working directory; morituri will change to this directory " "and files will be created relative to it when not absolute ") - # FIXME: have a cache of these pickles somewhere - self.parser.add_option('-T', '--toc-pickle', - action="store", dest="toc_pickle", - help="pickle to use for reading and writing the TOC") rcommon.addTemplate(self) - self.parser.add_option('-R', '--release-id', - action="store", dest="release_id", - help="MusicBrainz release id to match to (if there are multiple)") - default = 'flac' # here to avoid import gst eating our options @@ -125,151 +250,55 @@ Log files will log the path to tracks relative to this directory. if self.options.output_directory is None: self.options.output_directory = os.getcwd() - def do(self, args): - prog = program.Program(record=self.getRootCommand().record, - stdout=self.stdout) - runner = task.SyncRunner() - - def function(r, t): - r.run(t) - - # if the device is mounted (data session), unmount it - device = self.parentCommand.options.device - self.stdout.write('Checking device %s\n' % device) - - prog.setWorkingDirectory(self.options.working_directory) - prog.loadDevice(device) - prog.unmountDevice(device) - - version = None - - # first, read the normal TOC, which is fast - ptoc = cache.Persister(self.options.toc_pickle or None) - if not ptoc.object: - tries = 0 - while True: - tries += 1 - t = cdrdao.ReadTOCTask(device=device) - try: - function(runner, t) - break - except: - if tries > 3: - raise - self.debug('failed to read TOC after %d tries, retrying' % tries) - - version = t.tasks[1].parser.version - from pkg_resources import parse_version as V - # we've built a cdrdao 1.2.3rc2 modified package with the patch - if V(version) < V('1.2.3rc2p1'): - self.stdout.write('Warning: cdrdao older than 1.2.3 has a ' - 'pre-gap length bug.\n' - 'See http://sourceforge.net/tracker/?func=detail' - '&aid=604751&group_id=2171&atid=102171\n') - ptoc.persist(t.table) - ittoc = ptoc.object - assert ittoc.hasTOC() - - # already show us some info based on this - prog.getRipResult(ittoc.getCDDBDiscId()) - self.stdout.write("CDDB disc id: %s\n" % ittoc.getCDDBDiscId()) - mbdiscid = ittoc.getMusicBrainzDiscId() - self.stdout.write("MusicBrainz disc id %s\n" % mbdiscid) - - self.stdout.write("MusicBrainz lookup URL %s\n" % - ittoc.getMusicBrainzSubmitURL()) - - prog.metadata = prog.getMusicBrainz(ittoc, mbdiscid, - release=self.options.release_id) - - if not prog.metadata: - # fall back to FreeDB for lookup - cddbid = ittoc.getCDDBValues() - cddbmd = prog.getCDDB(cddbid) - if cddbmd: - self.stdout.write('FreeDB identifies disc as %s\n' % cddbmd) - - if not self.options.unknown: - prog.ejectDevice(device) - return -1 - - # now, read the complete index table, which is slower - itable = prog.getTable(runner, ittoc.getCDDBDiscId(), device) - - assert itable.getCDDBDiscId() == ittoc.getCDDBDiscId(), \ - "full table's id %s differs from toc id %s" % ( - itable.getCDDBDiscId(), ittoc.getCDDBDiscId()) - assert itable.getMusicBrainzDiscId() == ittoc.getMusicBrainzDiscId(), \ - "full table's mb id %s differs from toc id mb %s" % ( - itable.getMusicBrainzDiscId(), ittoc.getMusicBrainzDiscId()) - assert itable.getAccurateRipURL() == ittoc.getAccurateRipURL(), \ - "full table's AR URL %s differs from toc AR URL %s" % ( - itable.getAccurateRipURL(), ittoc.getAccurateRipURL()) - - prog.outdir = self.options.output_directory - prog.outdir = prog.outdir.decode('utf-8') + def doCommand(self): # here to avoid import gst eating our options from morituri.common import encode profile = encode.PROFILES[self.options.profile]() - - # result - - prog.result.cdrdaoVersion = version - prog.result.cdparanoiaVersion = cdparanoia.getCdParanoiaVersion() - info = drive.getDeviceInfo(self.parentCommand.options.device) - if info: - try: - prog.result.cdparanoiaDefeatsCache = self.getRootCommand( - ).config.getDefeatsCache(*info) - except KeyError, e: - self.debug('Got key error: %r' % (e, )) - prog.result.offset = int(self.options.offset) - prog.result.artist = prog.metadata and prog.metadata.artist \ - or 'Unknown Artist' - prog.result.title = prog.metadata and prog.metadata.title \ - or 'Unknown Title' - # cdio is optional for now - try: - import cdio - _, prog.result.vendor, prog.result.model, prog.result.release = \ - cdio.Device(device).get_hwinfo() - except ImportError: - self.stdout.write( - 'WARNING: pycdio not installed, cannot identify drive\n') - prog.result.vendor = 'Unknown' - prog.result.model = 'Unknown' - prog.result.release = 'Unknown' - - prog.result.profileName = profile.name - prog.result.profilePipeline = profile.pipeline + self.program.result.profileName = profile.name + self.program.result.profilePipeline = profile.pipeline elementFactory = profile.pipeline.split(' ')[0] - prog.result.gstreamerVersion = gstreamer.gstreamerVersion() - prog.result.gstPythonVersion = gstreamer.gstPythonVersion() - prog.result.encoderVersion = gstreamer.elementFactoryVersion( + self.program.result.gstreamerVersion = gstreamer.gstreamerVersion() + self.program.result.gstPythonVersion = gstreamer.gstPythonVersion() + self.program.result.encoderVersion = gstreamer.elementFactoryVersion( elementFactory) + self.program.setWorkingDirectory(self.options.working_directory) + self.program.outdir = self.options.output_directory.decode('utf-8') + self.program.result.offset = int(self.options.offset) + + ### write disc files + discName = self.program.getPath(self.program.outdir, self.options.disc_template, + self.mbdiscid, 0, profile=profile) + dirname = os.path.dirname(discName) + if not os.path.exists(dirname): + self.stdout.write("Creating output directory %s\n" % dirname) + os.makedirs(dirname) + # FIXME: say when we're continuing a rip + # FIXME: disambiguate if the pre-existing rip is different + + # FIXME: turn this into a method def ripIfNotRipped(number): self.debug('ripIfNotRipped for track %d' % number) # we can have a previous result - trackResult = prog.result.getTrackResult(number) + trackResult = self.program.result.getTrackResult(number) if not trackResult: trackResult = result.TrackResult() - prog.result.tracks.append(trackResult) + self.program.result.tracks.append(trackResult) else: self.debug('ripIfNotRipped have trackresult, path %r' % trackResult.filename) - path = prog.getPath(prog.outdir, self.options.track_template, - mbdiscid, number, profile=profile) + '.' + profile.extension + path = self.program.getPath(self.program.outdir, self.options.track_template, + self.mbdiscid, number, profile=profile) + '.' + profile.extension self.debug('ripIfNotRipped: path %r' % path) trackResult.number = number assert type(path) is unicode, "%r is not unicode" % path trackResult.filename = path if number > 0: - trackResult.pregap = itable.tracks[number - 1].getPregap() + trackResult.pregap = self.itable.tracks[number - 1].getPregap() # FIXME: optionally allow overriding reripping if os.path.exists(path): @@ -280,9 +309,9 @@ Log files will log the path to tracks relative to this directory. trackResult.filename, path)) self.stdout.write('Verifying track %d of %d: %s\n' % ( - number, len(itable.tracks), + number, len(self.itable.tracks), os.path.basename(path).encode('utf-8'))) - if not prog.verifyTrack(runner, trackResult): + if not self.program.verifyTrack(self.runner, trackResult): self.stdout.write('Verification failed, reripping...\n') os.unlink(path) @@ -293,20 +322,20 @@ Log files will log the path to tracks relative to this directory. trackResult.testduration = 0.0 trackResult.copyduration = 0.0 self.stdout.write('Ripping track %d of %d: %s\n' % ( - number, len(itable.tracks), + number, len(self.itable.tracks), os.path.basename(path).encode('utf-8'))) while tries < MAX_TRIES: tries += 1 try: self.debug('ripIfNotRipped: track %d, try %d', number, tries) - prog.ripTrack(runner, trackResult, + self.program.ripTrack(self.runner, trackResult, offset=int(self.options.offset), device=self.parentCommand.options.device, profile=profile, - taglist=prog.getTagList(number), + taglist=self.program.getTagList(number), what='track %d of %d' % ( - number, len(itable.tracks))) + number, len(self.itable.tracks))) break except Exception, e: self.debug('Got exception %r on try %d', @@ -333,18 +362,18 @@ Log files will log the path to tracks relative to this directory. # overlay this rip onto the Table if number == 0: # HTOA goes on index 0 of track 1 - itable.setFile(1, 0, trackResult.filename, - ittoc.getTrackStart(1), number) + self.itable.setFile(1, 0, trackResult.filename, + self.ittoc.getTrackStart(1), number) else: - itable.setFile(number, 1, trackResult.filename, - ittoc.getTrackLength(number), number) + self.itable.setFile(number, 1, trackResult.filename, + self.ittoc.getTrackLength(number), number) - prog.saveRipResult() + self.program.saveRipResult() # check for hidden track one audio htoapath = None - htoa = prog.getHTOA() + htoa = self.program.getHTOA() if htoa: start, stop = htoa self.stdout.write( @@ -353,9 +382,9 @@ Log files will log the path to tracks relative to this directory. # rip it ripIfNotRipped(0) - htoapath = prog.result.tracks[0].filename + htoapath = self.program.result.tracks[0].filename - for i, track in enumerate(itable.tracks): + for i, track in enumerate(self.itable.tracks): # FIXME: rip data tracks differently if not track.audio: self.stdout.write( @@ -368,14 +397,14 @@ Log files will log the path to tracks relative to this directory. ripIfNotRipped(i + 1) ### write disc files - discName = prog.getPath(prog.outdir, self.options.disc_template, - mbdiscid, 0, profile=profile) + discName = self.program.getPath(self.program.outdir, self.options.disc_template, + self.mbdiscid, 0, profile=profile) dirname = os.path.dirname(discName) if not os.path.exists(dirname): os.makedirs(dirname) self.debug('writing cue file for %r', discName) - prog.writeCue(discName) + self.program.writeCue(discName) # write .m3u file self.debug('writing m3u file for %r', discName) @@ -393,21 +422,21 @@ Log files will log the path to tracks relative to this directory. if htoapath: writeFile(handle, htoapath, - itable.getTrackStart(1) / common.FRAMES_PER_SECOND) + self.itable.getTrackStart(1) / common.FRAMES_PER_SECOND) - for i, track in enumerate(itable.tracks): + for i, track in enumerate(self.itable.tracks): if not track.audio: continue - path = prog.getPath(prog.outdir, self.options.track_template, - mbdiscid, i + 1, profile=profile) + '.' + profile.extension + path = self.program.getPath(self.program.outdir, self.options.track_template, + self.mbdiscid, i + 1, profile=profile) + '.' + profile.extension writeFile(handle, path, - itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND) + self.itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND) handle.close() # verify using accuraterip - url = ittoc.getAccurateRipURL() + url = self.ittoc.getAccurateRipURL() self.stdout.write("AccurateRip URL %s\n" % url) accucache = accurip.AccuCache() @@ -420,34 +449,34 @@ Log files will log the path to tracks relative to this directory. self.stdout.write('%d AccurateRip reponses found\n' % len(responses)) - if responses[0].cddbDiscId != itable.getCDDBDiscId(): + if responses[0].cddbDiscId != self.itable.getCDDBDiscId(): self.stdout.write( "AccurateRip response discid different: %s\n" % responses[0].cddbDiscId) - prog.verifyImage(runner, responses) + self.program.verifyImage(self.runner, responses) - self.stdout.write("\n".join(prog.getAccurateRipResults()) + "\n") + self.stdout.write("\n".join(self.program.getAccurateRipResults()) + "\n") - prog.saveRipResult() + self.program.saveRipResult() # write log file try: klazz = result.getLoggers()[self.options.logger] - prog.writeLog(discName, klazz()) + self.program.writeLog(discName, klazz()) except KeyError: self.stderr.write("No logger named %s found!\n" % ( self.options.logger)) - prog.ejectDevice(device) + self.program.ejectDevice(self.device) class CD(logcommand.LogCommand): summary = "handle CD's" - subCommandClasses = [Rip, ] + subCommandClasses = [Info, Rip, ] def addOptions(self): self.parser.add_option('-d', '--device',