root/ndsnamer.py

Revision 39, 11.6 kB (checked in by jmoiron, 5 months ago)

fixes to various libraries

  • Property svn:executable set to *
Line 
1 #!/usr/bin/env python
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         # clean the dat if that was asked of us
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         # lists
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         # prime identity reporting
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             # for now, this should always be 1
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         # what do we need to read the whole file for?
168         # CRC definitely, for Header we need to read most of it
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             #DatFile.sanity_check ensures this relationship between position and number
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             #else: self.datfile.export()
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
Note: See TracBrowser for help on using the browser.