#!/usr/bin/env python import sys, os, re from romutil import log from romutil import fsutil from romutil import romlib from romutil import ndscodes from romutil import datfile from romutil import arclib from romutil import identify from optparse import OptionParser, OptionGroup log.options['rjust_width'] = 8 """Help on Identify Options: There are many methods of identifying a rom: some are trustworthy, and some are less-so. The methods used by this program are listed below. They are listed in order of decreasing Trust/Speed. Trust Speed +-------------------------------+--------------------------------+ CRC comparisson w/ DAT | #### - filename number ROM Gamecode | filename <-> DAT comp ROM Banner/Gamename comp | ROM Gamecode filename <-> DAT comp | ROM Banner/Gamename comp #### - filename number | CRC comparisson w/ DAT +-------------------------------+--------------------------------+ In reality, since most images are going to be compressed, anything that involves looking at the rom header is going to be orders of magnitude slower than just dealing with the filename. In addition to this, peeking at the header will always be faster than performing the CRC check on the file. Some of these are prone to false negatives, some to false positives. In addition, because there are multiple regions, and region information is not always encoded reliably in filenames, the #### match might produce a valid result that is not deterministically verifiable by the filename comparissons. -i/--identify: identify by speed until 2 identifications agree -S/--slow : identify based on trust (first match) -F/--fast : identify based on speed (first match) Please be careful and use -p/--pretend when using -F. DAT and SCENE numbering often differ. """ _NAME_HELP = """Help on Name Format String The naming string (which can be given on the command line with -n) can have the following format characters: %# rom number (default to 4 digits; %### for 3, etc) %n rom name (ex. "Puzzle Series Vol. 8 - Nankuro") %r region (1 letter, ex. "J" for Japan) %R region (2 letters ex. "EU" for Europe) %g release group (ex. "Independent", "SirVG") %c CRC (lowercase, ex. "e9ee9c7d") %C CRC (uppercase, ex. "E9EE9C7D") The default string is: "%# - %n (%r)" """ def excsafe(f): def inner(self, *args): try: return f(self, *args) except Exception, v: import traceback log.warn(str(v) + ' (%s)' % f.__name__) traceback.print_exc() return [] return inner class _main: def __init__(self): self.usage = "usage: %prog [options] datfile [file.nds, ...]" self.version = "%prog 0.1" parser = OptionParser(usage=self.usage, version=self.version) parser.add_option('-c', '--crc', action='store_true', dest='crc', help='perform file crc checking') parser.add_option('-k', '--dat-clean', action='store_true', dest='dat_clean', help='remove MAX Media Launcher, reorder, etc') parser.add_option('-r', '--rename', action='store_true', dest='rename', help='perform rom renaming') identg = OptionGroup(parser, "Identication/Naming") identg.add_option('-n', '--name', dest='name', help='use provided name format (see --help-name)') identg.add_option('', '--help-name', action='store_true', dest='help_name', help='extended help on name format') report = OptionGroup(parser, "Reporting Options") report.add_option('-m', '--missing', action='store_true', dest='missing', help='show missing roms') report.add_option('', '--header', action='store_true', dest='header', help='print header information (lots of output)') generl = OptionGroup(parser, "General Options") generl.add_option('-g', '--range', metavar="RANGE", dest='range', help='perform only over range (#-#)') generl.add_option('-v', '--verbose', action='count', dest='verbose', help='verbose level (use multiple to increase verbosity)') generl.add_option('-d', '--debug', action='store_true', dest='debug', help='enable debug output') generl.add_option('-p', '--pretend', action='store_true', dest='pretend', help='do not perform any file modifications') parser.add_option_group(identg) parser.add_option_group(report) parser.add_option_group(generl) (self.options, self.args) = parser.parse_args() options, args = self.options, self.args if options.help_name: if options.help_name: print _NAME_HELP sys.exit(0) self.resolve_conflicting_options() if not len(args): parser.print_help() sys.exit(0) log.options['debug'] = options.debug self.datfile_name = fsutil.normalize_path(args[0]) self.romfile_names = fsutil.paths_to_list(args[1:]) types = arclib.supported_extensions + ('nds',) self.romfile_names = fsutil.filter_by_types(self.romfile_names, types, case_sensitive=False) self.romfile_names.sort() self.parser = parser # clean the dat if that was asked of us self.datfile = self.readDatFile() if options.dat_clean: self.doDatClean() if not self.romfile_names: if options.dat_clean: return if not options.dat_clean: log.error('rom filenames required (searching for: %s)' % str(types)) # lists self.range = self._getRange() self.roms = self._getRoms() self.name = self._getName() if not self.roms: log.v('no roms that met supplied criteria') return # prime identity reporting self.have = [] log.options['verbose'] = self.options.verbose identify.VERBOSE = self.options.verbose identify.v = lambda s: log.v(s) identify.vv = lambda s: log.vv(s) for rom in self.roms: log.v('checking ' + repr(rom)) games = self.doIdentify(rom) # for now, this should always be 1 if len(games) != 1: continue game = games[0] self.have.append(game) if self.options.crc: self.doCheckCRC(rom) if self.options.rename: self.doRomRename(rom, game) self.doMissing() # what do we need to read the whole file for? # CRC definitely, for Header we need to read most of it def resolve_conflicting_options(self): """Resolve conflicting command line options. If one is found, exit.""" if self.options.name and not self.options.rename: log.error('--name requires --rename') def readDatFile(self): """Try to read the datfile argument. If there is a failure, exit.""" try: df = datfile.openDat(self.datfile_name) log.vv('Found datfile of type <%s>' % df.dftype) return df except datfile.DatFileException, msg: log.error(str(msg) + ' (is the first argument a datfile?)') def _getRange(self): if self.options.range: spl = self.options.range.split('-') if len(spl) != 2: log.error('range [%s] invalid; must be in form ##-## (no spaces)' % self.options.range) try: lower = int(spl[0]) upper = int(spl[1]) + 1 except ValueError: log.error('range [%s] invalid; range must be valid integers' % self.options.range) return range(lower, upper) def _getRoms(self): """Try to create rom objects for each file. If a range was specified, do not return roms that are in that range.""" r = [] for filename in self.romfile_names: rom = romlib.Rom(filename) if not self.options.range or rom.number in self.range: r.append(rom) else: log.vv('rom "%s" not in given range' % rom.number) return r def _getName(self): """Resolve what naming scheme we should use. We should do some filtering so that we don't end up with extra % things beyond what we have.""" if not self.options.name: log.vv('using default naming format "%# - %n (%r)"') return "%# - %n (%r)" else: return self.options.name def _formatName(self, rom, game): fmt = str(self.name) fmt = fmt.replace('%n', game.clean_name) fmt = fmt.replace('%c', game.crc.lower()) fmt = fmt.replace('%C', game.crc.upper()) fmt = fmt.replace('%r', ndscodes.map_region_1l(game.region)) fmt = fmt.replace('%R', ndscodes.map_region_2l(game.region)) if game.group and fmt.rfind("%g") >= 0: fmt = fmt.replace('%g', game.group) nre = re.compile(r'%#+') match = nre.search(fmt) if match: nums = match.group() padding = len(nums) - 1 if len(nums) == 2: strfmt = "%04d" else: strfmt = "%%0%dd" % padding fmt = fmt.replace(nums, strfmt % game.number) fmt += '.' + rom.extension return fmt def doDatClean(self): """Clean a DAT file to correspond to scene numbers (remove MAX Media, NinjaPass, etc)""" prelen = len(self.datfile.games) for item in datfile.phr_hwo_list: number, name = item[0], item[1].lower() #DatFile.sanity_check ensures this relationship between position and number game = self.datfile.games[number-1] if game.name.lower().rfind(name) >= 0: self.datfile.remove_game(game.number) log.v('removed game "%s" (#%04d)' % (game.name, game.number)) if len(self.datfile.games) != prelen: log.vv('writing datfile > %s' % (self.datfile_name)) if not self.options.pretend: self.datfile.export(open(self.datfile_name, 'w')) #else: self.datfile.export() else: log.vv('datfile was already clean') @excsafe def doIdentify(self, rom): """Identify the rom using the proper speed/trust.""" candidates = identify.identify(rom, self.datfile.games) if not candidates: log.p('Could not identify: %s [%s]' % (rom.filename, rom.crc)) return [] for candidate in candidates: log.vv('ID: %r : %s' % (rom, candidate.name)) return candidates @excsafe def doCheckCRC(self, rom): crc = rom.get_crc() for game in self.datfile.games: if crc.lower() == game.crc.lower(): log.p(' OK (%s)' % (game.name), 'CRC > ') return log.p(' BAD (%s)' % (game.name), 'CRC > ') def doRomRename(self, rom, game): new_name = self._formatName(rom, game) if rom.filename == new_name: return log.p("NAME > %s -> %s " % (rom.filename, new_name)) if not self.options.pretend: os.rename(rom.path, os.path.join(rom.dirpath, new_name)) def doMissing(self): if not self.options.missing: return missing = [] for game in self.datfile.games: if game not in self.have: missing.append(game) for game in missing: log.p("MISS > %s" % (game)) if not missing: log.p("All %d have been identified." % len(self.datfile.games)) if __name__ == '__main__': _main() sys.exit()