Changeset 49

Show
Ignore:
Timestamp:
12/30/07 04:09:31 (1 year ago)
Author:
jmoiron
Message:

lots of added features

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • misc/lurker.py

    r42 r49  
    11#!/usr/bin/env python 
    22 
    3 import urllib, urllib2, sys, BeautifulSoup 
     3import urllib, urllib2, sys, re 
     4 
     5PROG = "lurker.py" 
     6VERSION = "0.1a" 
     7DEBUG = True 
     8 
     9# we require BeautifulSoup 
     10try: 
     11    import BeautifulSoup 
     12except ImportError: 
     13    print "Error: BeautifulSoup is required for %s" % (PROG) 
     14    print "  You can install it via easyinstall: easy_install beautifulsoup" 
     15    sys.exit(-1) 
     16     
     17if DEBUG: import unittest 
     18 
     19def dbg(s): 
     20    print 'dbg> %s' % s 
    421 
    522enc = urllib.urlencode 
     
    825search = 'http://gotlurk.net/?action=dosearch' 
    926 
     27# regex patterns 
     28# matches v#### after a '_' or ' ' 
     29vol = re.compile(r'[ _](?P<vol>v[0-9]{1,4})') 
     30# matches c#### or -#### 
     31chap = re.compile(r'(?P<chap>[c\-]{1}[0-9]{1,4})') 
     32 
     33### Generic Utilities ### 
     34 
     35def numToList(string): 
     36    """converts a string like "3,5,7-9,14" into a list of ints 
     37    raises InvalidRange 
     38    raises InvalidInteger""" 
     39    ret = [] 
     40    numsplit = string.split(",") 
     41    # the following code makes nums into a list of all integers 
     42    for n in numsplit: 
     43        nr = n.split('-') 
     44        # handle the case of a single number 
     45        if len(nr) == 1: 
     46            try: ret.append(int(n)) 
     47            except: raise ValueError("number (%s)" % n) 
     48        # handle the case of a range 
     49        elif len(nr) == 2: 
     50            try: 
     51                low = int(nr[0]) 
     52                high = int(nr[1]) + 1 
     53                if low > high: raise ValueError("number (%s)" % nr) 
     54                ret += range(low, high) 
     55            except ValueError: raise ValueError("number (%s)" % nr) 
     56        else: raise ValueError("range") 
     57    return ret 
     58 
     59def sizeToBytes(string): 
     60    """Converts 'human' sizes like 11K/23.1M/1.11G to bytes.""" 
     61 
     62### Juicy Bits ### 
     63 
    1064def get_cookie(): 
     65    'need a valid session to search at lurk...' 
    1166    res = urllib2.urlopen(home) 
    1267    if 'set-cookie' in res.headers.keys(): 
    1368        return res.headers['set-cookie'].split(';')[0] 
    1469 
    15 cookie = get_cookie() # yum 
     70cookie = False 
    1671 
    1772def do_query(querystr): 
     73    'does a query on lurk, returns a list of results created by soup_results' 
    1874    query = enc({'sstr' : querystr}) 
    1975 
     
    3692        results.append({ 
    3793            'bot': tds[0].a.string, 
    38             'pack': '#'+tds[2].string
    39             'gets': tds[3].string
     94            'pack': int(tds[2].string)
     95            'gets': int(tds[3].string.strip('x'))
    4096            'size': tds[4].string, 
    4197            'name': tds[5].string}) 
    4298    return results 
    4399 
     100def parse_name(name): 
     101    coax = lambda x: int(x.strip('-_cv')) 
     102    v = vol.search(name) 
     103    c = chap.search(name) 
     104    ret = {'vol' : None, 'chap' : None} 
     105    if v: ret['vol'] = coax(v.group('vol')) 
     106    if c: ret['chap'] = coax(c.group('chap')) 
     107    return ret 
     108 
     109class FilterFormatExc(Exception): pass 
     110 
     111def num_filter(pstr): 
     112    stripped = pstr.strip('!<>') 
     113    pl = numToList(stripped) 
     114    if len(pl) > 1: 
     115        # we are dealing with a range here;  only '!' or none are legal 
     116        if pstr.startswith('!'): 
     117            return lambda x: int(x) not in pl 
     118        elif pstr != stripped: 
     119            raise FilterFormatExc('Only "!" is allowed with multiple numbers') 
     120        return lambda x: int(x) in pl 
     121    # okay, there is only 1 number, all '!<>' are legal 
     122    # if we don't use '!<>' 
     123    if stripped == pstr: 
     124        return lambda x: int(stripped) == int(x) 
     125    elif pstr.startswith('!'): 
     126        return lambda x: int(x) != int(stripped) 
     127    elif pstr.startswith('>'): 
     128        return lambda x: int(x) > int(stripped) 
     129    elif pstr.startswith('<'): 
     130        return lambda x: int(x) < int(stripped) 
     131 
     132    raise FilterFormatExc('Only "!><" are allowed with a single number') 
     133     
     134def build_filter(pstr, num=True): 
     135    if num: return num_filter(pstr) 
     136    return lambda x: pstr.lower() not in x.lower() 
     137 
     138def pack_desc(pack): 
     139    'turn a pack into its desc string' 
     140    desc = ' pack #%s on %s (%s gets @ %s)' % (pack['pack'], pack['bot'], pack['gets'], pack['size']) 
     141    name = pack['name'].ljust(40) 
     142    return name + desc 
     143     
     144def apply_filter(filt, x): 
     145    try: return filt(x) 
     146    except: return True 
     147 
     148def main(): 
     149    global cookie 
     150    sf, bf, vf, cf = False, False, False, False 
     151    opts, querystr = handleOptions() 
     152    if opts.string: 
     153        sf = build_filter(opts.string, False) 
     154    if opts.bot: 
     155        _bf = build_filter(opts.bot, False) 
     156        bf = lambda x: not _bf(x) 
     157    if opts.vol: 
     158        dbg('Filter: %s' % opts.vol) 
     159        try: vf = build_filter(opts.vol) 
     160        except FilterFormatExc, ex: 
     161            print "Error: %s" % ex 
     162            sys.exit(-1) 
     163    if opts.chap: 
     164        try: cf = build_filter(opts.chap) 
     165        except FilterFormatExc, ex: 
     166            print "Error: %s" % ex 
     167            sys.exit(-1) 
     168    dbg('query: <%s>' % querystr) 
     169    cookie = get_cookie() # yum 
     170    dbg('cookie: <%r>' % cookie) 
     171    results = do_query(querystr) 
     172    if sf: results = filter(lambda x: apply_filter(sf, pack_desc(x)), results) 
     173    if bf: results = filter(lambda x: apply_filter(bf, x['bot']), results) 
     174    if vf: results = filter(lambda x: apply_filter(vf, parse_name(x['name'])['vol']), results) 
     175    if cf: results = filter(lambda x: apply_filter(cf, parse_name(x['name'])['chap']), results) 
     176    if opts.onlyvols: 
     177        results = filter(lambda x: parse_name(x['name'])['vol'] != None, results) 
     178    if opts.onlychaps: 
     179        results = filter(lambda x: parse_name(x['name'])['chap'] != None, results) 
     180    for item in results: 
     181        print pack_desc(item) 
     182     
     183    if opts.xdccq: 
     184        bots = set() 
     185        for item in results: bots.add(item['bot']) 
     186        for bot in bots: 
     187            packs = [p['pack'] for p in results if p['bot'] == bot] 
     188            print 'xdccq get %s %s' % (bot, ','.join(map(str, packs))) 
     189     
     190 
     191def handleOptions(): 
     192    from optparse import OptionParser, OptionGroup 
     193     
     194    parser = OptionParser(usage='%prog [options] Query', version=VERSION) 
     195    parser.add_option('-x', '--xdccq', action='store_true', dest='xdccq', help='output for use in xdccq') 
     196 
     197    group_filter = OptionGroup(parser, "Filtering options") 
     198    group_filter.add_option('-s', '--string', action='store', dest='string', help='filter out by string match') 
     199    group_filter.add_option('-b', '--bot', action='store', dest='bot', help='filter bots') 
     200    group_filter.add_option('-v', '--vol', action='store', dest='vol', help='vilter volumes') 
     201    group_filter.add_option('-c', '--chap', action='store', dest='chap', help='filter chapters') 
     202    group_filter.add_option('-V', '--vols-only', action='store_true', dest='onlyvols', help='only show volumes') 
     203    group_filter.add_option('-C', '--chaps-only', action='store_true', dest='onlychaps', help='only show chapters') 
     204     
     205    group_debug = OptionGroup(parser, "Debugging options") 
     206    group_debug.add_option('', '--test', action='store_true', dest='test', help='run unit tests') 
     207 
     208    parser.add_option_group(group_filter) 
     209    parser.add_option_group(group_debug) 
     210 
     211    options, args = parser.parse_args() 
     212    querystr = ' '.join(args).strip() 
     213     
     214    # if we have requested the unit test, run it and exit 
     215    if options.test: 
     216        for arg in sys.argv: sys.argv.remove(arg) 
     217        dotests() 
     218     
     219    # if we lack a query string, print the usage and exit 
     220    if not querystr: 
     221        parser.print_usage() 
     222        sys.exit(-1) 
     223 
     224    return options, querystr 
     225 
     226### Testing ### 
     227 
     228testStrings = [ 
     229    ("Hajime_no_Ippo_v70_c02[SC].zip", 70, 2), 
     230    ("Migiko_Nippon_Ichi[solaris-svu].zip", None, None), 
     231    ("Tsuki_no_Shippo_v04_c23[ShoujoCrusade][e-scans].zip", 4, 23), 
     232    ("One_Piece_c477[FH].zip", None, 477), 
     233    ("One_Piece_ c479[OPHQ].zip", None, 479), 
     234    ("One_Piece-442[Null][KEFI].zip", None, 442) 
     235] 
     236 
     237testNumtolistStrings = [ 
     238    ("1", [1]), 
     239    ("1,2", [1,2]), 
     240    ("1,3-9,11", [1,3,4,5,6,7,8,9,11]) 
     241] 
     242 
     243testFilters = [ 
     244    ("!234", [1,2,234,5], [1,2,5]), 
     245    ("<40", [38,39,40,41,42], [38,39]), 
     246    (">10", [7,8,9,10,11,12], [11,12]), 
     247    ("!5-8", [4,5,6,7,8,9], [4,9]) 
     248] 
     249 
     250testStrFilter = [ 
     251    ("fo", ['foo','faz','feo','fo'], ['faz','feo']) 
     252] 
     253 
     254# yes, negative numbers are not allowed 
     255testNumToListException = ["10,5-2", "11,G,3", "11,-3,-1"] 
     256 
     257def listcmp(l,r): 
     258    if len(l) != len(r): return False 
     259    for item in zip(l,r): 
     260        if item[0] != item[1]: return False 
     261    return True 
     262 
     263class TestRegexMatching(unittest.TestCase): 
     264    def testMatching(self): 
     265        for s in testStrings: 
     266            m = parse_name(s[0]) 
     267            self.assertEqual(m['vol'], s[1]) 
     268            self.assertEqual(m['chap'], s[2]) 
     269 
     270#TODO: test that bad patterns fail properly 
     271class TestFilterApplication(unittest.TestCase): 
     272    def testNumberFiltering(self): 
     273        for s in testFilters: 
     274            filt = build_filter(s[0], True) 
     275            l = filter(filt, s[1]) 
     276            self.assertEquals(True, listcmp(l, s[2])) 
     277     
     278    def testStrFiltering(self): 
     279        for s in testStrFilter: 
     280            filt = build_filter(s[0], False) 
     281            l = filter(filt, s[1]) 
     282            self.assertEquals(True, listcmp(l, s[2])) 
     283             
     284 
     285class TestNumtolist(unittest.TestCase): 
     286    def testNumtolist(self): 
     287        for s in testNumtolistStrings: 
     288            l = numToList(s[0]) 
     289            self.assertEquals(True, listcmp(l, s[1])) 
     290     
     291    def testNumtolistExc(self): 
     292        s = testNumToListException 
     293        self.assertRaises(ValueError, numToList, s[0]) 
     294        self.assertRaises(ValueError, numToList, s[1]) 
     295        self.assertRaises(ValueError, numToList, s[2]) 
     296 
     297def dotests(): 
     298    unittest.main() 
     299    sys.exit(0) 
     300 
    44301if __name__ == '__main__': 
    45     from optparse import OptionParser 
    46  
    47     parser = OptionParser(usage='%prog [query]', version='0.1') 
    48     (options, args) = parser.parse_args() 
    49     querystr = ' '.join(args) 
    50  
    51     results = do_query(querystr) 
    52     for item in results: 
    53         name = item['name'].ljust(40) 
    54         desc = 'pack %s on %s (%s gets @ %s)' % (item['pack'], item['bot'], item['gets'], item['size']) 
    55         print name + desc 
     302    main() 
     303