| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
import sys, os, re |
|---|
| 4 |
from romutil import log |
|---|
| 5 |
from romutil import fsutil |
|---|
| 6 |
from romutil import romlib |
|---|
| 7 |
from romutil import ndscodes |
|---|
| 8 |
from romutil import datfile |
|---|
| 9 |
from romutil import arclib |
|---|
| 10 |
from romutil import identify |
|---|
| 11 |
|
|---|
| 12 |
from optparse import OptionParser, OptionGroup |
|---|
| 13 |
|
|---|
| 14 |
log.options['rjust_width'] = 8 |
|---|
| 15 |
|
|---|
| 16 |
"""Help on Identify Options: |
|---|
| 17 |
|
|---|
| 18 |
There are many methods of identifying a rom: some are trustworthy, and |
|---|
| 19 |
some are less-so. The methods used by this program are listed below. |
|---|
| 20 |
They are listed in order of decreasing Trust/Speed. |
|---|
| 21 |
Trust Speed |
|---|
| 22 |
+-------------------------------+--------------------------------+ |
|---|
| 23 |
CRC comparisson w/ DAT | #### - filename number |
|---|
| 24 |
ROM Gamecode | filename <-> DAT comp |
|---|
| 25 |
ROM Banner/Gamename comp | ROM Gamecode |
|---|
| 26 |
filename <-> DAT comp | ROM Banner/Gamename comp |
|---|
| 27 |
#### - filename number | CRC comparisson w/ DAT |
|---|
| 28 |
+-------------------------------+--------------------------------+ |
|---|
| 29 |
|
|---|
| 30 |
In reality, since most images are going to be compressed, anything that |
|---|
| 31 |
involves looking at the rom header is going to be orders of magnitude |
|---|
| 32 |
slower than just dealing with the filename. In addition to this, peeking |
|---|
| 33 |
at the header will always be faster than performing the CRC check on the |
|---|
| 34 |
file. Some of these are prone to false negatives, some to false positives. |
|---|
| 35 |
|
|---|
| 36 |
In addition, because there are multiple regions, and region information is |
|---|
| 37 |
not always encoded reliably in filenames, the #### match might produce a |
|---|
| 38 |
valid result that is not deterministically verifiable by the filename |
|---|
| 39 |
comparissons. |
|---|
| 40 |
|
|---|
| 41 |
-i/--identify: identify by speed until 2 identifications agree |
|---|
| 42 |
-S/--slow : identify based on trust (first match) |
|---|
| 43 |
-F/--fast : identify based on speed (first match) |
|---|
| 44 |
|
|---|
| 45 |
Please be careful and use -p/--pretend when using -F. DAT and SCENE |
|---|
| 46 |
numbering often differ. |
|---|
| 47 |
""" |
|---|
| 48 |
|
|---|
| 49 |
_NAME_HELP = """Help on Name Format String |
|---|
| 50 |
|
|---|
| 51 |
The naming string (which can be given on the command line with -n) can |
|---|
| 52 |
have the following format characters: |
|---|
| 53 |
|
|---|
| 54 |
%# rom number (default to 4 digits; %### for 3, etc) |
|---|
| 55 |
%n rom name (ex. "Puzzle Series Vol. 8 - Nankuro") |
|---|
| 56 |
%r region (1 letter, ex. "J" for Japan) |
|---|
| 57 |
%R region (2 letters ex. "EU" for Europe) |
|---|
| 58 |
%g release group (ex. "Independent", "SirVG") |
|---|
| 59 |
%c CRC (lowercase, ex. "e9ee9c7d") |
|---|
| 60 |
%C CRC (uppercase, ex. "E9EE9C7D") |
|---|
| 61 |
|
|---|
| 62 |
The default string is: "%# - %n (%r)" |
|---|
| 63 |
""" |
|---|
| 64 |
|
|---|
| 65 |
def excsafe(f): |
|---|
| 66 |
def inner(self, *args): |
|---|
| 67 |
try: |
|---|
| 68 |
return f(self, *args) |
|---|
| 69 |
except Exception, v: |
|---|
| 70 |
import traceback |
|---|
| 71 |
log.warn(str(v) + ' (%s)' % f.__name__) |
|---|
| 72 |
traceback.print_exc() |
|---|
| 73 |
return [] |
|---|
| 74 |
return inner |
|---|
| 75 |
|
|---|
| 76 |
class _main: |
|---|
| 77 |
def __init__(self): |
|---|
| 78 |
self.usage = "usage: %prog [options] datfile [file.nds, ...]" |
|---|
| 79 |
self.version = "%prog 0.1" |
|---|
| 80 |
|
|---|
| 81 |
parser = OptionParser(usage=self.usage, version=self.version) |
|---|
| 82 |
parser.add_option('-c', '--crc', action='store_true', dest='crc', help='perform file crc checking') |
|---|
| 83 |
parser.add_option('-k', '--dat-clean', action='store_true', dest='dat_clean', help='remove MAX Media Launcher, reorder, etc') |
|---|
| 84 |
parser.add_option('-r', '--rename', action='store_true', dest='rename', help='perform rom renaming') |
|---|
| 85 |
|
|---|
| 86 |
identg = OptionGroup(parser, "Identication/Naming") |
|---|
| 87 |
identg.add_option('-n', '--name', dest='name', help='use provided name format (see --help-name)') |
|---|
| 88 |
identg.add_option('', '--help-name', action='store_true', dest='help_name', help='extended help on name format') |
|---|
| 89 |
|
|---|
| 90 |
report = OptionGroup(parser, "Reporting Options") |
|---|
| 91 |
report.add_option('-m', '--missing', action='store_true', dest='missing', help='show missing roms') |
|---|
| 92 |
report.add_option('', '--header', action='store_true', dest='header', help='print header information (lots of output)') |
|---|
| 93 |
|
|---|
| 94 |
generl = OptionGroup(parser, "General Options") |
|---|
| 95 |
generl.add_option('-g', '--range', metavar="RANGE", dest='range', help='perform only over range (#-#)') |
|---|
| 96 |
generl.add_option('-v', '--verbose', action='count', dest='verbose', help='verbose level (use multiple to increase verbosity)') |
|---|
| 97 |
generl.add_option('-d', '--debug', action='store_true', dest='debug', help='enable debug output') |
|---|
| 98 |
generl.add_option('-p', '--pretend', action='store_true', dest='pretend', help='do not perform any file modifications') |
|---|
| 99 |
|
|---|
| 100 |
parser.add_option_group(identg) |
|---|
| 101 |
parser.add_option_group(report) |
|---|
| 102 |
parser.add_option_group(generl) |
|---|
| 103 |
|
|---|
| 104 |
(self.options, self.args) = parser.parse_args() |
|---|
| 105 |
options, args = self.options, self.args |
|---|
| 106 |
|
|---|
| 107 |
if options.help_name: |
|---|
| 108 |
if options.help_name: print _NAME_HELP |
|---|
| 109 |
sys.exit(0) |
|---|
| 110 |
|
|---|
| 111 |
self.resolve_conflicting_options() |
|---|
| 112 |
if not len(args): |
|---|
| 113 |
parser.print_help() |
|---|
| 114 |
sys.exit(0) |
|---|
| 115 |
|
|---|
| 116 |
log.options['debug'] = options.debug |
|---|
| 117 |
|
|---|
| 118 |
self.datfile_name = fsutil.normalize_path(args[0]) |
|---|
| 119 |
self.romfile_names = fsutil.paths_to_list(args[1:]) |
|---|
| 120 |
types = arclib.supported_extensions + ('nds',) |
|---|
| 121 |
self.romfile_names = fsutil.filter_by_types(self.romfile_names, types, case_sensitive=False) |
|---|
| 122 |
self.romfile_names.sort() |
|---|
| 123 |
self.parser = parser |
|---|
| 124 |
|
|---|
| 125 |
|
|---|
| 126 |
self.datfile = self.readDatFile() |
|---|
| 127 |
|
|---|
| 128 |
if options.dat_clean: |
|---|
| 129 |
self.doDatClean() |
|---|
| 130 |
|
|---|
| 131 |
if not self.romfile_names: |
|---|
| 132 |
if options.dat_clean: return |
|---|
| 133 |
if not options.dat_clean: |
|---|
| 134 |
log.error('rom filenames required (searching for: %s)' % str(types)) |
|---|
| 135 |
|
|---|
| 136 |
|
|---|
| 137 |
self.range = self._getRange() |
|---|
| 138 |
self.roms = self._getRoms() |
|---|
| 139 |
self.name = self._getName() |
|---|
| 140 |
|
|---|
| 141 |
if not self.roms: |
|---|
| 142 |
log.v('no roms that met supplied criteria') |
|---|
| 143 |
return |
|---|
| 144 |
|
|---|
| 145 |
|
|---|
| 146 |
self.have = [] |
|---|
| 147 |
log.options['verbose'] = self.options.verbose |
|---|
| 148 |
identify.VERBOSE = self.options.verbose |
|---|
| 149 |
identify.v = lambda s: log.v(s) |
|---|
| 150 |
identify.vv = lambda s: log.vv(s) |
|---|
| 151 |
|
|---|
| 152 |
for rom in self.roms: |
|---|
| 153 |
log.v('checking ' + repr(rom)) |
|---|
| 154 |
games = self.doIdentify(rom) |
|---|
| 155 |
|
|---|
| 156 |
if len(games) != 1: |
|---|
| 157 |
continue |
|---|
| 158 |
game = games[0] |
|---|
| 159 |
self.have.append(game) |
|---|
| 160 |
if self.options.crc: |
|---|
| 161 |
self.doCheckCRC(rom) |
|---|
| 162 |
if self.options.rename: |
|---|
| 163 |
self.doRomRename(rom, game) |
|---|
| 164 |
|
|---|
| 165 |
self.doMissing() |
|---|
| 166 |
|
|---|
| 167 |
|
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 |
def resolve_conflicting_options(self): |
|---|
| 171 |
"""Resolve conflicting command line options. If one is found, exit.""" |
|---|
| 172 |
if self.options.name and not self.options.rename: |
|---|
| 173 |
log.error('--name requires --rename') |
|---|
| 174 |
|
|---|
| 175 |
def readDatFile(self): |
|---|
| 176 |
"""Try to read the datfile argument. If there is a failure, exit.""" |
|---|
| 177 |
try: |
|---|
| 178 |
df = datfile.openDat(self.datfile_name) |
|---|
| 179 |
log.vv('Found datfile of type <%s>' % df.dftype) |
|---|
| 180 |
return df |
|---|
| 181 |
except datfile.DatFileException, msg: |
|---|
| 182 |
log.error(str(msg) + ' (is the first argument a datfile?)') |
|---|
| 183 |
|
|---|
| 184 |
def _getRange(self): |
|---|
| 185 |
if self.options.range: |
|---|
| 186 |
spl = self.options.range.split('-') |
|---|
| 187 |
if len(spl) != 2: |
|---|
| 188 |
log.error('range [%s] invalid; must be in form ##-## (no spaces)' % self.options.range) |
|---|
| 189 |
try: |
|---|
| 190 |
lower = int(spl[0]) |
|---|
| 191 |
upper = int(spl[1]) + 1 |
|---|
| 192 |
except ValueError: |
|---|
| 193 |
log.error('range [%s] invalid; range must be valid integers' % self.options.range) |
|---|
| 194 |
return range(lower, upper) |
|---|
| 195 |
|
|---|
| 196 |
def _getRoms(self): |
|---|
| 197 |
"""Try to create rom objects for each file. If a range was specified, |
|---|
| 198 |
do not return roms that are in that range.""" |
|---|
| 199 |
r = [] |
|---|
| 200 |
for filename in self.romfile_names: |
|---|
| 201 |
rom = romlib.Rom(filename) |
|---|
| 202 |
if not self.options.range or rom.number in self.range: |
|---|
| 203 |
r.append(rom) |
|---|
| 204 |
else: |
|---|
| 205 |
log.vv('rom "%s" not in given range' % rom.number) |
|---|
| 206 |
return r |
|---|
| 207 |
|
|---|
| 208 |
def _getName(self): |
|---|
| 209 |
"""Resolve what naming scheme we should use. We should do some filtering so |
|---|
| 210 |
that we don't end up with extra % things beyond what we have.""" |
|---|
| 211 |
if not self.options.name: |
|---|
| 212 |
log.vv('using default naming format "%# - %n (%r)"') |
|---|
| 213 |
return "%# - %n (%r)" |
|---|
| 214 |
else: return self.options.name |
|---|
| 215 |
|
|---|
| 216 |
def _formatName(self, rom, game): |
|---|
| 217 |
fmt = str(self.name) |
|---|
| 218 |
fmt = fmt.replace('%n', game.clean_name) |
|---|
| 219 |
fmt = fmt.replace('%c', game.crc.lower()) |
|---|
| 220 |
fmt = fmt.replace('%C', game.crc.upper()) |
|---|
| 221 |
fmt = fmt.replace('%r', ndscodes.map_region_1l(game.region)) |
|---|
| 222 |
fmt = fmt.replace('%R', ndscodes.map_region_2l(game.region)) |
|---|
| 223 |
if game.group and fmt.rfind("%g") >= 0: |
|---|
| 224 |
fmt = fmt.replace('%g', game.group) |
|---|
| 225 |
nre = re.compile(r'%#+') |
|---|
| 226 |
match = nre.search(fmt) |
|---|
| 227 |
if match: |
|---|
| 228 |
nums = match.group() |
|---|
| 229 |
padding = len(nums) - 1 |
|---|
| 230 |
if len(nums) == 2: |
|---|
| 231 |
strfmt = "%04d" |
|---|
| 232 |
else: |
|---|
| 233 |
strfmt = "%%0%dd" % padding |
|---|
| 234 |
fmt = fmt.replace(nums, strfmt % game.number) |
|---|
| 235 |
fmt += '.' + rom.extension |
|---|
| 236 |
return fmt |
|---|
| 237 |
|
|---|
| 238 |
def doDatClean(self): |
|---|
| 239 |
"""Clean a DAT file to correspond to scene numbers (remove MAX Media, NinjaPass, etc)""" |
|---|
| 240 |
prelen = len(self.datfile.games) |
|---|
| 241 |
for item in datfile.phr_hwo_list: |
|---|
| 242 |
number, name = item[0], item[1].lower() |
|---|
| 243 |
|
|---|
| 244 |
game = self.datfile.games[number-1] |
|---|
| 245 |
if game.name.lower().rfind(name) >= 0: |
|---|
| 246 |
self.datfile.remove_game(game.number) |
|---|
| 247 |
log.v('removed game "%s" (#%04d)' % (game.name, game.number)) |
|---|
| 248 |
|
|---|
| 249 |
if len(self.datfile.games) != prelen: |
|---|
| 250 |
log.vv('writing datfile > %s' % (self.datfile_name)) |
|---|
| 251 |
if not self.options.pretend: |
|---|
| 252 |
self.datfile.export(open(self.datfile_name, 'w')) |
|---|
| 253 |
|
|---|
| 254 |
else: |
|---|
| 255 |
log.vv('datfile was already clean') |
|---|
| 256 |
|
|---|
| 257 |
@excsafe |
|---|
| 258 |
def doIdentify(self, rom): |
|---|
| 259 |
"""Identify the rom using the proper speed/trust.""" |
|---|
| 260 |
candidates = identify.identify(rom, self.datfile.games) |
|---|
| 261 |
if not candidates: |
|---|
| 262 |
log.p('Could not identify: %s [%s]' % (rom.filename, rom.crc)) |
|---|
| 263 |
return [] |
|---|
| 264 |
for candidate in candidates: |
|---|
| 265 |
log.vv('ID: %r : %s' % (rom, candidate.name)) |
|---|
| 266 |
return candidates |
|---|
| 267 |
|
|---|
| 268 |
@excsafe |
|---|
| 269 |
def doCheckCRC(self, rom): |
|---|
| 270 |
crc = rom.get_crc() |
|---|
| 271 |
for game in self.datfile.games: |
|---|
| 272 |
if crc.lower() == game.crc.lower(): |
|---|
| 273 |
log.p(' OK (%s)' % (game.name), 'CRC > ') |
|---|
| 274 |
return |
|---|
| 275 |
log.p(' BAD (%s)' % (game.name), 'CRC > ') |
|---|
| 276 |
|
|---|
| 277 |
def doRomRename(self, rom, game): |
|---|
| 278 |
new_name = self._formatName(rom, game) |
|---|
| 279 |
if rom.filename == new_name: |
|---|
| 280 |
return |
|---|
| 281 |
log.p("NAME > %s -> %s " % (rom.filename, new_name)) |
|---|
| 282 |
if not self.options.pretend: |
|---|
| 283 |
os.rename(rom.path, os.path.join(rom.dirpath, new_name)) |
|---|
| 284 |
|
|---|
| 285 |
def doMissing(self): |
|---|
| 286 |
if not self.options.missing: return |
|---|
| 287 |
missing = [] |
|---|
| 288 |
for game in self.datfile.games: |
|---|
| 289 |
if game not in self.have: |
|---|
| 290 |
missing.append(game) |
|---|
| 291 |
for game in missing: |
|---|
| 292 |
log.p("MISS > %s" % (game)) |
|---|
| 293 |
if not missing: |
|---|
| 294 |
log.p("All %d have been identified." % len(self.datfile.games)) |
|---|
| 295 |
|
|---|
| 296 |
|
|---|
| 297 |
if __name__ == '__main__': |
|---|
| 298 |
_main() |
|---|
| 299 |
sys.exit() |
|---|
| 300 |
|
|---|