diff --git a/pirate/pirate.py b/pirate/pirate.py index b3c6383..b740b00 100755 --- a/pirate/pirate.py +++ b/pirate/pirate.py @@ -66,6 +66,43 @@ def parse_cmd(cmd, url): return ret_no_quotes +def parse_torrent_command(l): + # Very permissive handling + # Check for any occurances or d, f, p, t, m, or q + cmd_code_match = re.search(r'([hdfpmtq])', l, + flags=re.IGNORECASE) + if cmd_code_match: + code = cmd_code_match.group(0).lower() + else: + code = None + + # Clean up command codes + # Substitute multiple consecutive spaces/commas for single + # comma remove anything that isn't an integer or comma. + # Turn into list + l = re.sub(r'^[hdfp, ]*|[hdfp, ]*$', '', l) + l = re.sub('[ ,]+', ',', l) + l = re.sub('[^0-9,-]', '', l) + parsed_input = l.split(',') + + # expand ranges + choices = [] + # loop will generate a list of lists + for elem in parsed_input: + left, sep, right = elem.partition('-') + if right: + choices.append(list(range(int(left), int(right) + 1))) + elif left != '': + choices.append([int(left)]) + + # flatten list + choices = sum(choices, []) + # the current code stores the choices as strings + # instead of ints. not sure if necessary + choices = [elem for elem in choices] + return code, choices + + def main(): config = load_config() @@ -91,7 +128,7 @@ def main(): help='list Sortable Types') parser.add_argument('-L', '--local', dest='database', help='an xml file containing the Pirate Bay database') - parser.add_argument('-p', dest='pages', default=1, + parser.add_argument('-p', dest='pages', default=1, type=int, help='the number of pages to fetch ' "(doesn't work with --local)") parser.add_argument('-0', dest='first', @@ -123,6 +160,16 @@ def main(): help='disable colored output') args = parser.parse_args() + # figure out the mode - browse, search, top or recent + if args.browse: + args.mode = 'browse' + elif args.recent: + args.mode = 'recent' + elif len(args.search) == 0: + args.mode = 'top' + else: + args.mode = 'search' + if (config.getboolean('Misc', 'colors') and not args.color or not config.getboolean('Misc', 'colors')): pirate.data.colored_output = False @@ -161,11 +208,11 @@ def main(): path = args.database else: path = config.get('LocalDB', 'path') - mags = pirate.local.search(path, args.search) + results = pirate.local.search(path, args.search) sizes, uploaded = [], [] else: - mags, mirrors = [], {'https://thepiratebay.mn'} + results, mirrors = [], {'https://thepiratebay.mn'} try: req = request.Request('https://proxybay.co/list.txt', headers=pirate.data.default_headers) @@ -181,9 +228,15 @@ def main(): for mirror in mirrors: try: - print('Trying', mirror, end='... ') - mags, sizes, uploaded, ids = pirate.torrent.remote(args, - mirror) + print('Trying', mirror, end='... \n') + results = pirate.torrent.remote( + pages=args.pages, + category=pirate.torrent.parse_category(args.category), + sort=pirate.torrent.parse_sort(args.sort), + mode=args.mode, + terms=args.search, + mirror=mirror + ) except (urllib.error.URLError, socket.timeout, IOError, ValueError): print('Failed', color='WARN') @@ -195,18 +248,18 @@ def main(): print('No available mirrors :(', color='WARN') return - if not mags: + if len(results) == 0: print('No results') return - pirate.print.search_results(mags, sizes, uploaded, local=args.database) + pirate.print.search_results(results, local=args.database) if args.first: print('Choosing first result') choices = [0] elif args.download_all: print('Downloading all results') - choices = range(len(mags)) + choices = range(len(results)) else: # New input loop to support different link options while True: @@ -219,40 +272,7 @@ def main(): return try: - # Very permissive handling - # Check for any occurances or d, f, p, t, m, or q - cmd_code_match = re.search(r'([hdfpmtq])', l, - flags=re.IGNORECASE) - if cmd_code_match: - code = cmd_code_match.group(0).lower() - else: - code = None - - # Clean up command codes - # Substitute multiple consecutive spaces/commas for single - # comma remove anything that isn't an integer or comma. - # Turn into list - l = re.sub(r'^[hdfp, ]*|[hdfp, ]*$', '', l) - l = re.sub('[ ,]+', ',', l) - l = re.sub('[^0-9,-]', '', l) - parsed_input = l.split(',') - - # expand ranges - choices = [] - # loop will generate a list of lists - for elem in parsed_input: - left, sep, right = elem.partition('-') - if right: - choices.append(list(range(int(left), int(right) + 1))) - elif left != '': - choices.append([int(left)]) - - # flatten list - choices = sum(choices, []) - # the current code stores the choices as strings - # instead of ints. not sure if necessary - choices = [str(elem) for elem in choices] - + code, choices = parse_torrent_command(l) # Act on option, if supplied print('') if code == 'h': @@ -268,16 +288,16 @@ def main(): print('Bye.', color='alt') return elif code == 'd': - pirate.print.descriptions(choices, mags, site, ids) + pirate.print.descriptions(choices, results, site) elif code == 'f': - pirate.print.file_lists(choices, mags, site, ids) + pirate.print.file_lists(choices, results, site) elif code == 'p': - pirate.print.search_results(mags, sizes, uploaded) + pirate.print.search_results(results) elif code == 'm': - pirate.torrent.save_magnets(choices, mags, config.get( + pirate.torrent.save_magnets(choices, results, config.get( 'Save', 'directory')) elif code == 't': - pirate.torrent.save_torrents(choices, mags, config.get( + pirate.torrent.save_torrents(choices, results, config.get( 'Save', 'directory')) elif not l: print('No links entered!', color='WARN') @@ -291,13 +311,13 @@ def main(): if args.save_magnets or config.getboolean('Save', 'magnets'): print('Saving selected magnets...') - pirate.torrent.save_magnets(choices, mags, config.get( + pirate.torrent.save_magnets(choices, results, config.get( 'Save', 'directory')) save_to_file = True if args.save_torrents or config.getboolean('Save', 'torrents'): print('Saving selected torrents...') - pirate.torrent.save_torrents(choices, mags, config.get( + pirate.torrent.save_torrents(choices, results, config.get( 'Save', 'directory')) save_to_file = True @@ -305,7 +325,7 @@ def main(): return for choice in choices: - url = mags[int(choice)][0] + url = results[int(choice)]['magnet'] if args.transmission or config.getboolean('Misc', 'transmission'): subprocess.call(transmission_command + ['--add', url]) diff --git a/pirate/print.py b/pirate/print.py index 9d3f4a0..a83b89b 100644 --- a/pirate/print.py +++ b/pirate/print.py @@ -5,6 +5,7 @@ import gzip import colorama import urllib.parse as parse import urllib.request as request +import shutil from io import BytesIO import pirate.data @@ -31,8 +32,9 @@ def print(*args, **kwargs): return builtins.print(*args, **kwargs) -def search_results(mags, sizes, uploaded, local=None): - columns = int(os.popen('stty size', 'r').read().split()[1]) +# TODO: extract the name from the search results instead of the magnet link when possible +def search_results(results, local=None): + columns = shutil.get_terminal_size((80, 20)).columns cur_color = 'zebra_0' if local: @@ -45,21 +47,26 @@ def search_results(mags, sizes, uploaded, local=None): 'SIZE', 'UPLOAD', 'NAME', length=columns - 52), color='header') - for m, magnet in enumerate(mags): + for n, result in enumerate(results): # Alternate between colors cur_color = 'zebra_0' if cur_color == 'zebra_1' else 'zebra_1' - name = re.search(r'dn=([^\&]*)', magnet[0]) - torrent_name = parse.unquote(name.group(1)).replace('+', ' ') + name = re.search(r'dn=([^\&]*)', result['magnet']) + torrent_name = parse.unquote_plus(name.group(1)) if local: line = '{:5} {:{length}}' - content = [m, torrent_name[:columns]] + content = [n, torrent_name[:columns]] else: - no_seeders, no_leechers = map(int, magnet[1:]) - size, unit = (float(sizes[m][0]), - sizes[m][1]) if sizes else (0, '???') - date = uploaded[m] + no_seeders = int(result['seeds']) + no_leechers = int(result['leechers']) + if result['size'] != []: + size = float(result['size'][0]) + unit = result['size'][1] + else: + size = 0 + unit = '???' + date = result['uploaded'] # compute the S/L ratio (Higher is better) try: @@ -69,17 +76,16 @@ def search_results(mags, sizes, uploaded, local=None): line = ('{:4} {:5} {:5} {:5.1f} {:5.1f}' ' {:3} {:<11} {:{length}}') - content = [m, no_seeders, no_leechers, ratio, + content = [n, no_seeders, no_leechers, ratio, size, unit, date, torrent_name[:columns - 52]] # enhanced print output with justified columns print(line.format(*content, length=columns - 52), color=cur_color) -def descriptions(chosen_links, mags, site, identifiers): +def descriptions(chosen_links, results, site): for link in chosen_links: - link = int(link) - path = '/torrent/%s/' % identifiers[link] + path = '/torrent/%s/' % results[link]['id'] req = request.Request(site + path, headers=pirate.data.default_headers) req.add_header('Accept-encoding', 'gzip') f = request.urlopen(req, timeout=pirate.data.default_timeout) @@ -88,7 +94,7 @@ def descriptions(chosen_links, mags, site, identifiers): f = gzip.GzipFile(fileobj=BytesIO(f.read())) res = f.read().decode('utf-8') - name = re.search(r'dn=([^\&]*)', mags[link][0]) + name = re.search(r'dn=([^\&]*)', results[link]['magnet']) torrent_name = parse.unquote(name.group(1)).replace('+', ' ') desc = re.search(r'
\s*
(.+?)(?=
)', res, re.DOTALL).group(1) @@ -101,10 +107,10 @@ def descriptions(chosen_links, mags, site, identifiers): print(desc, color='zebra_0') -def file_lists(chosen_links, mags, site, identifiers): +def file_lists(chosen_links, results, site): for link in chosen_links: path = '/ajax_details_filelist.php' - query = '?id=' + identifiers[int(link)] + query = '?id=' + results[link]['id'] req = request.Request(site + path + query, headers=pirate.data.default_headers) req.add_header('Accept-encoding', 'gzip') @@ -113,10 +119,14 @@ def file_lists(chosen_links, mags, site, identifiers): if f.info().get('Content-Encoding') == 'gzip': f = gzip.GzipFile(fileobj=BytesIO(f.read())) + # TODO: proper html decoding/parsing res = f.read().decode('utf-8').replace(' ', ' ') + if 'File list not available.' in res: + print('File list not available.') + return files = re.findall(r'\s*([^<]+?)\s*\s*([^<]+?)\s*', res) - name = re.search(r'dn=([^\&]*)', mags[int(link)][0]) + name = re.search(r'dn=([^\&]*)', results[link]['magnet']) torrent_name = parse.unquote(name.group(1)).replace('+', ' ') print('Files in "%s":' % torrent_name, color='zebra_1') diff --git a/pirate/torrent.py b/pirate/torrent.py index 6699b88..3c0aa16 100644 --- a/pirate/torrent.py +++ b/pirate/torrent.py @@ -6,57 +6,130 @@ import urllib.parse as parse import urllib.error import os.path +from pyquery import PyQuery as pq + import pirate.data from pirate.print import print from io import BytesIO -#todo: redo this with html parser instead of regex -def remote(args, mirror): + +parser_regex = r'"(magnet\:\?xt=[^"]*)|([^<]+)' + + +def parse_category(category): + try: + category = int(category) + except ValueError: + pass + if category in pirate.data.categories.values(): + return category + elif category in pirate.data.categories.keys(): + return pirate.data.categories[category] + else: + print('Invalid category ignored', color='WARN') + return '0' + + +def parse_sort(sort): + try: + sort = int(sort) + except ValueError: + pass + if sort in pirate.data.sorts.values(): + return sort + elif sort in pirate.data.sorts.keys(): + return pirate.data.sorts[sort] + else: + print('Invalid sort ignored', color='WARN') + return '99' + + +#TODO: warn users when using a sort in a mode that doesn't accept sorts +#TODO: warn users when using search terms in a mode that doesn't accept search terms +#TODO: same with page parameter for top and top48h +#TODO: warn the user if trying to use a minor category with top48h +def build_request_path(page, category, sort, mode, terms): + if mode == 'browse': + if(category == 0): + category = 100 + return '/browse/{}/{}/{}'.format(category, page, sort) + elif mode == 'recent': + # This is not a typo. There is no / between 48h and the category. + path = '/top/48h' + # only major categories can be used with this mode + if(category == 0): + return path + 'all' + else: + return path + str(category) + elif mode == 'top': + path = '/top/' + if(category == 0): + return path + 'all' + else: + return path + str(category) + elif mode == 'search': + query = urllib.parse.quote_plus(' '.join(terms)) + return '/search/{}/{}/{}/{}'.format(query, page, sort, category) + else: + raise Exception('Unknown mode.') + + +# this returns a list of dictionaries +def parse_page(html): + d = pq(html) + + results = [] + # parse the rows one by one + for row in d('table#searchResult tr'): + drow = d(row) + if len(drow('th')) > 0: + continue + + # grab info about the row + magnet = pq(drow(':eq(0)>td:nth-child(2)>a:nth-child(2)')[0]).attr('href') + seeds = pq(drow(':eq(0)>td:nth-child(3)')).text() + leechers = pq(drow(':eq(0)>td:nth-child(4)')).text() + id_ = pq(drow('.detLink')).attr('href').split('/')[2] + + # parse descriptions separately + desc_text = pq(drow('font.detDesc')[0]).text() + size = re.findall(r'(?<=Size )[0-9.]+\s[KMGT]*[i ]*B', desc_text)[0].split() + uploaded = re.findall(r'(?<=Uploaded ).+(?=\, Size)', desc_text)[0] + + results.append({ + 'magnet': magnet, + 'seeds': seeds, + 'leechers': leechers, + 'size': size, + 'uploaded': uploaded, + 'id': id_ + }) + + # check for a blocked mirror + no_results = re.search(r'No hits\. Try adding an asterisk in ' + r'you search phrase\.', html) + if len(results) == 0 and no_results is None: + # Contradiction - we found no results, + # but the page didn't say there were no results. + # The page is probably not actually the pirate bay, + # so let's try another mirror + raise IOError('Blocked mirror detected.') + + return results + + +def remote(pages, category, sort, mode, terms, mirror): res_l = [] - pages = int(args.pages) + if pages < 1: raise ValueError('Please provide an integer greater than 0 ' 'for the number of pages to fetch.') - if str(args.category) in pirate.data.categories.values(): - category = args.category - elif args.category in pirate.data.categories.keys(): - category = pirate.data.categories[args.category] - else: - category = '0' - print('Invalid category ignored', color='WARN') - - if str(args.sort) in pirate.data.sorts.values(): - sort = args.sort - elif args.sort in pirate.data.sorts.keys(): - sort = pirate.data.sorts[args.sort] - else: - sort = '99' - print('Invalid sort ignored', color='WARN') # Catch the Ctrl-C exception and exit cleanly try: - sizes = [] - uploaded = [] - identifiers = [] for page in range(pages): - if args.browse: - path = '/browse/' - if(category == 0): - category = 100 - path = '/browse/' + '/'.join(str(i) for i in ( - category, page, sort)) - elif len(args.search) == 0: - path = '/top/48h' if args.recent else '/top/' - if(category == 0): - path += 'all' - else: - path += str(category) - else: - path = '/search/' + '/'.join(str(i) for i in ( - '+'.join(args.search), - page, sort, - category)) + path = build_request_path(page, category, sort, mode, terms) req = request.Request(mirror + path, headers=pirate.data.default_headers) @@ -65,53 +138,14 @@ def remote(args, mirror): if f.info().get('Content-Encoding') == 'gzip': f = gzip.GzipFile(fileobj=BytesIO(f.read())) res = f.read().decode('utf-8') - found = re.findall(r'"(magnet\:\?xt=[^"]*)|' - r'([^<]+)', res) - # check for a blocked mirror - no_results = re.search(r'No hits\. Try adding an asterisk in ' - r'you search phrase\.', res) - if found == [] and no_results is None: - # Contradiction - we found no results, - # but the page didn't say there were no results. - # The page is probably not actually the pirate bay, - # so let's try another mirror - raise IOError('Blocked mirror detected.') + res_l += parse_page(res) - # get sizes as well and substitute the   character - sizes.extend([match.replace(' ', ' ').split() - for match in re.findall(r'(?<=Size )[0-9.]' - r'+\ \;[KMGT]*[i ]*B', res)]) - - uploaded.extend([match.replace(' ', ' ') - for match in re.findall(r'(?<=Uploaded )' - r'.+(?=\, Size)',res)]) - - identifiers.extend([match.replace(' ', ' ') - for match in re.findall('(?<=/torrent/)' - '[0-9]+(?=/)',res)]) - - state = 'seeds' - curr = ['', 0, 0] #magnet, seeds, leeches - for f in found: - if f[1] == '': - curr[0] = f[0] - else: - if state == 'seeds': - curr[1] = f[1] - state = 'leeches' - else: - curr[2] = f[1] - state = 'seeds' - res_l.append(curr) - curr = ['', 0, 0] - except KeyboardInterrupt : + except KeyboardInterrupt: print('\nCancelled.') sys.exit(0) - # return the sizes in a spearate list - return res_l, sizes, uploaded, identifiers - + return res_l def get_torrent(info_hash): @@ -127,9 +161,9 @@ def get_torrent(info_hash): return torrent.read() -def save_torrents(chosen_links, mags, folder): +def save_torrents(chosen_links, results, folder): for link in chosen_links: - magnet = mags[int(link)][0] + magnet = results[link]['magnet'] name = re.search(r'dn=([^\&]*)', magnet) torrent_name = parse.unquote(name.group(1)).replace('+', ' ') info_hash = int(re.search(r'btih:([a-f0-9]{40})', magnet).group(1), 16) @@ -146,7 +180,7 @@ def save_torrents(chosen_links, mags, folder): def save_magnets(chosen_links, mags, folder): for link in chosen_links: - magnet = mags[int(link)][0] + magnet = results[link]['magnet'] name = re.search(r'dn=([^\&]*)', magnet) torrent_name = parse.unquote(name.group(1)).replace('+', ' ') info_hash = int(re.search(r'btih:([a-f0-9]{40})', magnet).group(1), 16) diff --git a/setup.py b/setup.py index 8f8f6c1..42707ef 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup(name='pirate-get', entry_points={ 'console_scripts': ['pirate-get = pirate.pirate:main'] }, - install_requires=['colorama>=0.3.3'], + install_requires=['colorama>=0.3.3', 'pyquery>=1.2.9'], keywords=['torrent', 'magnet', 'download', 'tpb', 'client'], classifiers=[ 'Topic :: Utilities', diff --git a/tests/data/blocked.html b/tests/data/blocked.html new file mode 100644 index 0000000..dd4bc42 --- /dev/null +++ b/tests/data/blocked.html @@ -0,0 +1 @@ +blocked. diff --git a/tests/data/dan_bull_search.html b/tests/data/dan_bull_search.html new file mode 100644 index 0000000..b7799c8 --- /dev/null +++ b/tests/data/dan_bull_search.html @@ -0,0 +1,461 @@ + + + + The Pirate Bay - The galaxy's most resilient bittorrent site + + + + + + + + + + + + + + + + + + + + + + + + +

Search results: dan bull Displaying hits from 0 to 15 (approx 15 found)

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type
Name (Order by: Uploaded, Size, ULed by, SE, LE)
View: Single / Double 
SELE
+
+ Audio
+ (Music) +
+
+ +Magnet linkTrusted + Uploaded 04-04 2014, Size 89.33 MiB, ULed by Capajebo + 161
+
+ Audio
+ (Other) +
+
+ +Magnet linkThis torrent has 1 comments. + Uploaded 03-02 2014, Size 294 MiB, ULed by Vakume + 40
+
+ Audio
+ (Music) +
+
+ +Magnet link + Uploaded 01-19 2013, Size 54.86 MiB, ULed by blowingfish + 20
+
+ Audio
+ (Other) +
+
+ +Magnet linkThis torrent has 11 comments. + Uploaded 01-21 2010, Size 236.78 MiB, ULed by SuperSaru + 10
+
+ Audio
+ (Music) +
+
+ +Magnet link + Uploaded 09-02 2014, Size 36.27 MiB, ULed by Bazookus + 10
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 1 comments.VIP + Uploaded 09-27 2009, Size 5.51 MiB, ULed by oneanight + 01
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 1 comments. + Uploaded 11-29 2009, Size 5.07 MiB, ULed by epiclawl + 00
+
+ Audio
+ (Music) +
+
+ +Magnet link + Uploaded 11-10 2011, Size 5.34 MiB, ULed by Imperator42 + 00
+
+ Audio
+ (Music) +
+
+ +Magnet link + Uploaded 12-20 2011, Size 4.8 MiB, ULed by lerdie + 00
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 1 comments. + Uploaded 12-21 2011, Size 3.4 MiB, ULed by mattdow + 01
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 3 comments. + Uploaded 12-21 2011, Size 4.8 MiB, ULed by lerdie + 01
+
+ Audio
+ (Other) +
+
+ +Magnet linkThis torrent has 1 comments.VIP + Uploaded 03-09 2012, Size 60.72 MiB, ULed by oneanight + 01
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 1 comments. + Uploaded 10-24 2012, Size 6.29 MiB, ULed by PIRATE300 + 00
+
+ Audio
+ (Music) +
+
+ +Magnet linkThis torrent has 1 comments. + Uploaded 11-10 2012, Size 6.41 MiB, ULed by AdpoX10 + 00
+
+ Audio
+ (Other) +
+
+ +Magnet linkThis torrent has 2 comments. + Uploaded 01-19 2013, Size 54.87 MiB, ULed by blowingfish + 01
+
+
+
+ + + + + + \ No newline at end of file diff --git a/tests/data/no_hits.html b/tests/data/no_hits.html new file mode 100644 index 0000000..a3a9156 --- /dev/null +++ b/tests/data/no_hits.html @@ -0,0 +1,200 @@ + + + + The Pirate Bay - The galaxy's most resilient bittorrent site + + + + + + + + + + + + + + + + + + + + + + + + +

Search results: aaaaaaaaaaaaaaaaa No hits. Try adding an asterisk in you search phrase.

+ +
+ +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/tests/rich.xml b/tests/data/rich.xml similarity index 100% rename from tests/rich.xml rename to tests/data/rich.xml diff --git a/tests/test_local.py b/tests/test_local.py index 3d6b8d9..afda395 100755 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -3,10 +3,13 @@ import unittest import pirate.local import os +from tests import util + + class TestLocal(unittest.TestCase): def test_rich_xml(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'rich.xml') + path = util.data_path('rich.xml') expected = [['magnet:?xt=urn:btih:b03c8641415d3a0fc7077f5bf567634442989a74&dn=High.Chaparall.S02E02.PDTV.XViD.SWEDiSH-HuBBaTiX', '?', '?']] actual = pirate.local.search(path, ('High',)) self.assertEqual(actual, expected) diff --git a/tests/test_pirate.py b/tests/test_pirate.py new file mode 100755 index 0000000..a11f696 --- /dev/null +++ b/tests/test_pirate.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import unittest +import pirate.pirate + + +class TestPirate(unittest.TestCase): + + def test_parse_cmd(self): + tests = [ + [['abc', ''], ['abc']], + [['abc %s', 'url'], ['abc', 'url']], + [['abc "%s"', 'url'], ['abc', 'url']], + [["abc \'%s\'", 'url'], ['abc', 'url']], + [['abc bash -c "\'%s\'"', 'url'], ['abc', 'bash', '-c', "'url'"]], + [['abc %s %s', 'url'], ['abc', 'url', 'url']], + ] + for test in tests: + self.assertEqual(pirate.pirate.parse_cmd(*test[0]), test[1]) + + def test_parse_torrent_command(self): + tests = [ + [['h'], ('h', [])], + [['q'], ('q', [])], + [['d1'], ('d', [1])], + [['f1'], ('f', [1])], + [['p1'], ('p', [1])], + [['t1'], ('t', [1])], + [['m1'], ('m', [1])], + [['d 23'], ('d', [23])], + [['d 23,1'], ('d', [23, 1])], + [['d 23, 1'], ('d', [23, 1])], + [['1d'], ('d', [1])], + [['1 ... d'], ('d', [1])], + [['1-3 d'], ('d', [1,2,3])], + ] + for test in tests: + self.assertEqual(pirate.pirate.parse_torrent_command(*test[0]), test[1]) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_print.py b/tests/test_print.py new file mode 100755 index 0000000..438170d --- /dev/null +++ b/tests/test_print.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import unittest +from unittest.mock import patch +from unittest.mock import call + +import pirate.print + + +class TestPrint(unittest.TestCase): + + def test_print_results(self): + with patch('pirate.print.print') as mock: + results = [{ + 'magnet': 'dn=name', + 'seeds': 1, + 'leechers': 2, + 'size': ['3','MiB'], + 'uploaded': 'never' + }] + pirate.print.search_results(results) + actual = mock.call_args_list + expected = [ + call('LINK SEED LEECH RATIO SIZE UPLOAD NAME ', color='header'), + call(' 0 1 2 0.5 3.0 MiB never name ', color='zebra_1'), + ] + self.assertEqual(expected, actual) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_torrent.py b/tests/test_torrent.py new file mode 100755 index 0000000..6ca7c5c --- /dev/null +++ b/tests/test_torrent.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import unittest +import pirate.torrent +import os + +from tests import util + + +class TestTorrent(unittest.TestCase): + + def test_no_hits(self): + res = util.read_data('no_hits.html') + actual = pirate.torrent.parse_page(res) + expected = [] + self.assertEqual(actual, expected) + + def test_blocked_mirror(self): + res = util.read_data('blocked.html') + with self.assertRaises(IOError): + pirate.torrent.parse_page(res) + + def test_search_results(self): + res = util.read_data('dan_bull_search.html') + actual = pirate.torrent.parse_page(res) + expected = [{'uploaded': '04-04\xa02014', 'seeds': '16', 'leechers': '1', 'id': '9890864', 'magnet': 'magnet:?xt=urn:btih:30df4f8b42b8fd77f5e5aa34abbffe97f5e81fbf&dn=Dan+Croll+%26bull%3B+Sweet+Disarray+%5B2014%5D+320&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['89.33', 'MiB']}, {'uploaded': '03-02\xa02014', 'seeds': '4', 'leechers': '0', 'id': '9684858', 'magnet': 'magnet:?xt=urn:btih:7abd3eda600996b8e6fc9a61b83288e0c6ac0d83&dn=Dan+Bull+-+Massive+Collection&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['294', 'MiB']}, {'uploaded': '01-19\xa02013', 'seeds': '2', 'leechers': '0', 'id': '8037968', 'magnet': 'magnet:?xt=urn:btih:8f8d68fd0a51237c89692c428ed8a8f64a969c70&dn=Dan+Bull+-+Generation+Gaming+-+2013&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['54.86', 'MiB']}, {'uploaded': '01-21\xa02010', 'seeds': '1', 'leechers': '0', 'id': '5295449', 'magnet': 'magnet:?xt=urn:btih:3da6a0fdc1d67a768cb32597e926abdf3e1a2fdd&dn=Dan+Bull+Collection&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['236.78', 'MiB']}, {'uploaded': '09-02\xa02014', 'seeds': '1', 'leechers': '0', 'id': '10954408', 'magnet': 'magnet:?xt=urn:btih:5cd371a235317319db7da52c64422f9c2ac75d77&dn=Dan+Bull+-+The+Garden+%7B2014-Album%7D&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['36.27', 'MiB']}, {'uploaded': '09-27\xa02009', 'seeds': '0', 'leechers': '1', 'id': '5101630', 'magnet': 'magnet:?xt=urn:btih:4e14dbd077c920875be4c15971b23b609ad6716a&dn=Dan+Bull+-+Dear+Lily+%5Ban+open+letter+to+Lily+Allen%5D+-+2009%5BMP3+%40&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['5.51', 'MiB']}, {'uploaded': '11-29\xa02009', 'seeds': '0', 'leechers': '0', 'id': '5185893', 'magnet': 'magnet:?xt=urn:btih:5d9319cf852f7462422cb1bffc37b65174645047&dn=Dan+Bull+-+Dear+Mandy+%5Ban+open+letter+to+Lord+Mandelson%5D&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['5.07', 'MiB']}, {'uploaded': '11-10\xa02011', 'seeds': '0', 'leechers': '0', 'id': '6806996', 'magnet': 'magnet:?xt=urn:btih:1c54af57426f53fdef4bbf1a9dbddf32f7b4988a&dn=Dan+Bull+-+Dear+Lily+%28Lily+Allen%29+%28Song+about+filesharing%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['5.34', 'MiB']}, {'uploaded': '12-20\xa02011', 'seeds': '0', 'leechers': '0', 'id': '6901871', 'magnet': 'magnet:?xt=urn:btih:942c5bf3e1e9bc263939e13cea6ad7bd5f62aa36&dn=Dan+Bull+-+SOPA+Cabana.mp3&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['4.8', 'MiB']}, {'uploaded': '12-21\xa02011', 'seeds': '0', 'leechers': '1', 'id': '6902247', 'magnet': 'magnet:?xt=urn:btih:d376f68a31b0db652234e790ed7256ac5e32db57&dn=Dan+Bull+-+SOPA+Cabana&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['3.4', 'MiB']}, {'uploaded': '12-21\xa02011', 'seeds': '0', 'leechers': '1', 'id': '6903548', 'magnet': 'magnet:?xt=urn:btih:28163770a532eb24b9e0865878288a9bbdb7a5e6&dn=Dan+Bull+-+SOPA+Cabana+%5BWORKING%5D&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['4.8', 'MiB']}, {'uploaded': '03-09\xa02012', 'seeds': '0', 'leechers': '1', 'id': '7088979', 'magnet': 'magnet:?xt=urn:btih:779ab0f13a3fbb12ba68b27721491e4d143f26eb&dn=Dan+Bull+-+Bye+Bye+BPI+2012++%5BMP3%40192%5D%28oan%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['60.72', 'MiB']}, {'uploaded': '10-24\xa02012', 'seeds': '0', 'leechers': '0', 'id': '7756344', 'magnet': 'magnet:?xt=urn:btih:2667e4795bd5c868dedcabcb52943f4bb7212bab&dn=Dan+Bull+-+Dishonored+%5BExplicit+ver.%5D+%28Single+2012%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['6.29', 'MiB']}, {'uploaded': '11-10\xa02012', 'seeds': '0', 'leechers': '0', 'id': '7812951', 'magnet': 'magnet:?xt=urn:btih:16364f83c556ad0fd3bb57a4a7c890e7e8087414&dn=Halo+4+EPIC+Rap+By+Dan+Bull&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['6.41', 'MiB']}, {'uploaded': '01-19\xa02013', 'seeds': '0', 'leechers': '1', 'id': '8037899', 'magnet': 'magnet:?xt=urn:btih:843b466d9fd1f0bee3a476573b272dc2d6d0ebae&dn=Dan+Bull+-+Generation+Gaming+-+2013&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969', 'size': ['54.87', 'MiB']}] + self.assertEqual(actual, expected) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..26f0fbe --- /dev/null +++ b/tests/util.py @@ -0,0 +1,8 @@ +import os + +def data_path(name): + return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', name) + +def read_data(name): + with open(data_path(name)) as f: + return f.read()