1
0
mirror of https://github.com/vikstrous/pirate-get synced 2025-01-09 09:59:51 +01:00

Merge branch 'pypi'

This commit is contained in:
Viktor Stanchev 2015-08-29 23:59:15 -07:00
commit 36f4302cc3
19 changed files with 1005 additions and 735 deletions

66
.gitignore vendored
View File

@ -1 +1,65 @@
pirate-get-*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
.virtualenv
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# vim
*.swp
# setup.py
publish/*

10
circle.yml Normal file
View File

@ -0,0 +1,10 @@
machine:
python:
version: 3.4.2
test:
override:
- coverage run -m unittest discover
post:
- mkdir -p $CIRCLE_ARTIFACTS/coverage
- cd /home/ubuntu/pirate-get && coverage html --include=`pwd`* --omit="*/tests/*,*migrations*,*__init__*"
- cp -R /home/ubuntu/pirate-get/htmlcov/* $CIRCLE_ARTIFACTS/coverage

53
data/categories.json Normal file
View File

@ -0,0 +1,53 @@
{
"All": 0,
"Applications": 300,
"Applications/Android": 306,
"Applications/Handheld": 304,
"Applications/IOS (iPad/iPhone)": 305,
"Applications/Mac": 302,
"Applications/Other OS": 399,
"Applications/UNIX": 303,
"Applications/Windows": 301,
"Audio": 100,
"Audio/Audio books": 102,
"Audio/FLAC": 104,
"Audio/Music": 101,
"Audio/Other": 199,
"Audio/Sound clips": 103,
"Games": 400,
"Games/Android": 408,
"Games/Handheld": 406,
"Games/IOS (iPad/iPhone)": 407,
"Games/Mac": 402,
"Games/Other": 499,
"Games/PC": 401,
"Games/PSx": 403,
"Games/Wii": 405,
"Games/XBOX360": 404,
"Other": 600,
"Other/Comics": 602,
"Other/Covers": 604,
"Other/E-books": 601,
"Other/Other": 699,
"Other/Physibles": 605,
"Other/Pictures": 603,
"Porn": 500,
"Porn/Games": 504,
"Porn/HD - Movies": 505,
"Porn/Movie clips": 506,
"Porn/Movies": 501,
"Porn/Movies DVDR": 502,
"Porn/Other": 599,
"Porn/Pictures": 503,
"Video": 200,
"Video/3D": 209,
"Video/HD - Movies": 207,
"Video/HD - TV shows": 208,
"Video/Handheld": 206,
"Video/Movie clips": 204,
"Video/Movies": 201,
"Video/Movies DVDR": 202,
"Video/Music videos": 203,
"Video/Other": 299,
"Video/TV shows": 205
}

15
data/sorts.json Normal file
View File

@ -0,0 +1,15 @@
{
"TitleDsc": 1,
"TitleAsc": 2,
"DateDsc": 3,
"DateAsc": 4,
"SizeDsc": 5,
"SizeAsc": 6,
"SeedersDsc": 7,
"SeedersAsc": 8,
"LeechersDsc": 9,
"LeechersAsc": 10,
"CategoryDsc": 13,
"CategoryAsc": 14,
"Default": 99
}

23
install
View File

@ -1,23 +0,0 @@
#!/bin/sh
set -e
TMP=$(mktemp pirate-get-XXXXXX)
{
if [ $(which python3) ]
then
python='python3'
else
python='python'
fi
echo "#!/usr/bin/env $python" > "$TMP" &&
sed 1d $(dirname $0)/pirate-get.py >> "$TMP"
cp "$TMP" /usr/bin/pirate-get &&
chmod +x /usr/bin/pirate-get &&
chmod 755 /usr/bin/pirate-get &&
rm $TMP
} || rm $TMP

View File

@ -1,709 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2015, Viktor Stanchev and contributors
#
# This file is part of pirate-get.
#
# pirate-get is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pirate-get is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with pirate-get. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import re
import string
import gzip
import configparser
import argparse
import builtins
import subprocess
import webbrowser
import urllib.request as request
import urllib.parse as parse
from html.parser import HTMLParser
from urllib.error import URLError, HTTPError
from socket import timeout
from io import BytesIO
from os.path import expanduser, expandvars
colored_output = True
default_timeout = 10
default_headers = {'User-Agent': 'pirate get'}
categories = {
'All': 0,
'Applications': 300,
'Applications/Android': 306,
'Applications/Handheld': 304,
'Applications/IOS (iPad/iPhone)': 305,
'Applications/Mac': 302,
'Applications/Other OS': 399,
'Applications/UNIX': 303,
'Applications/Windows': 301,
'Audio': 100,
'Audio/Audio books': 102,
'Audio/FLAC': 104,
'Audio/Music': 101,
'Audio/Other': 199,
'Audio/Sound clips': 103,
'Games': 400,
'Games/Android': 408,
'Games/Handheld': 406,
'Games/IOS (iPad/iPhone)': 407,
'Games/Mac': 402,
'Games/Other': 499,
'Games/PC': 401,
'Games/PSx': 403,
'Games/Wii': 405,
'Games/XBOX360': 404,
'Other': 600,
'Other/Comics': 602,
'Other/Covers': 604,
'Other/E-books': 601,
'Other/Other': 699,
'Other/Physibles': 605,
'Other/Pictures': 603,
'Porn': 500,
'Porn/Games': 504,
'Porn/HD - Movies': 505,
'Porn/Movie clips': 506,
'Porn/Movies': 501,
'Porn/Movies DVDR': 502,
'Porn/Other': 599,
'Porn/Pictures': 503,
'Video': 200,
'Video/3D': 209,
'Video/HD - Movies': 207,
'Video/HD - TV shows': 208,
'Video/Handheld': 206,
'Video/Movie clips': 204,
'Video/Movies': 201,
'Video/Movies DVDR': 202,
'Video/Music videos': 203,
'Video/Other': 299,
'Video/TV shows': 205}
sorts = {
'TitleDsc': 1, 'TitleAsc': 2,
'DateDsc': 3, 'DateAsc': 4,
'SizeDsc': 5, 'SizeAsc': 6,
'SeedersDsc': 7, 'SeedersAsc': 8,
'LeechersDsc': 9, 'LeechersAsc': 10,
'CategoryDsc': 13, 'CategoryAsc': 14,
'Default': 99}
# create a subclass and override the handler methods
class BayParser(HTMLParser):
title = ''
q = ''
state = 'looking'
results = []
def __init__(self, q):
HTMLParser.__init__(self)
self.q = q.lower()
def handle_starttag(self, tag, attrs):
if tag == 'title':
self.state = 'title'
if tag == 'magnet' and self.state == 'matched':
self.state = 'magnet'
def handle_data(self, data):
if self.state == 'title':
if data.lower().find(self.q) != -1:
self.title = data
self.state = 'matched'
else:
self.state = 'looking'
if self.state == 'magnet':
self.results.append([
'magnet:?xt=urn:btih:' +
parse.quote(data) +
'&dn=' +
parse.quote(self.title), '?', '?'])
self.state = 'looking'
def print(*args, **kwargs):
if kwargs.get('color', False) and colored_output:
try:
import colorama
except (ImportError):
pass
else:
colorama.init()
color_dict = {
'default': '',
'header': colorama.Back.BLACK + colorama.Fore.WHITE,
'alt': colorama.Fore.YELLOW,
'zebra_0': '',
'zebra_1': colorama.Fore.BLUE,
'WARN': colorama.Fore.MAGENTA,
'ERROR': colorama.Fore.RED}
c = color_dict[kwargs.pop('color')]
args = (c + args[0],) + args[1:] + (colorama.Style.RESET_ALL,)
kwargs.pop('color', None)
return builtins.print(*args, **kwargs)
else:
kwargs.pop('color', None)
return builtins.print(*args, **kwargs)
def parse_cmd(cmd, url):
cmd_args_regex = r'''(('[^']*'|"[^"]*"|(\\\s|[^\s])+)+ *)'''
ret = re.findall(cmd_args_regex, cmd)
ret = [i[0].strip().replace('%s', url) for i in ret]
ret_no_quotes = []
for item in ret:
if (item[0] == "'" and item[-1] == "'") or (item[0] == '"' and item[-1] == '"'):
ret_no_quotes.append(item[1:-1])
else:
ret_no_quotes.append(item)
return ret_no_quotes
#todo: redo this with html parser instead of regex
def remote(args, 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 categories.values():
category = args.category
elif args.category in categories.keys():
category = categories[args.category]
else:
category = '0'
print('Invalid category ignored', color='WARN')
if str(args.sort) in sorts.values():
sort = args.sort
elif args.sort in sorts.keys():
sort = 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))
req = request.Request(mirror + path, headers=default_headers)
req.add_header('Accept-encoding', 'gzip')
f = request.urlopen(req, timeout=default_timeout)
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=[^"]*)|<td align="right">'
r'([^<]+)</td>', 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.')
# get sizes as well and substitute the &nbsp; character
sizes.extend([match.replace('&nbsp;', ' ').split()
for match in re.findall(r'(?<=Size )[0-9.]'
r'+\&nbsp\;[KMGT]*[i ]*B', res)])
uploaded.extend([match.replace('&nbsp;', ' ')
for match in re.findall(r'(?<=Uploaded )'
r'.+(?=\, Size)',res)])
identifiers.extend([match.replace('&nbsp;', ' ')
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 :
print('\nCancelled.')
sys.exit(0)
# return the sizes in a spearate list
return res_l, sizes, uploaded, identifiers
def local(db, search):
xml = open(db).readlines()
parser = BayParser(' '.join(search))
parser.feed(''.join(xml))
return parser.results
def load_config():
config = configparser.ConfigParser()
# default options
config.add_section('Save')
config.set('Save', 'magnets', 'false')
config.set('Save', 'torrents', 'false')
config.set('Save', 'directory', os.getcwd())
config.add_section('LocalDB')
config.set('LocalDB', 'enabled', 'false')
config.set('LocalDB', 'path', expanduser('~/downloads/pirate-get/db'))
config.add_section('Misc')
config.set('Misc', 'openCommand', '')
config.set('Misc', 'transmission', 'false')
config.set('Misc', 'colors', 'true')
# user-defined config files
main = expandvars('$XDG_CONFIG_HOME/pirate-get')
alt = expanduser('~/.config/pirate-get')
# read config file
config.read([main] if os.path.isfile(main) else [alt])
# expand env variables
directory = expanduser(expandvars(config.get('Save', 'Directory')))
path = expanduser(expandvars(config.get('LocalDB', 'path')))
config.set('Save', 'Directory', directory)
config.set('LocalDB', 'path', path)
return config
def get_torrent(info_hash):
url = 'http://torcache.net/torrent/{:X}.torrent'
req = request.Request(url.format(info_hash), headers=default_headers)
req.add_header('Accept-encoding', 'gzip')
torrent = request.urlopen(req, timeout=default_timeout)
if torrent.info().get('Content-Encoding') == 'gzip':
torrent = gzip.GzipFile(fileobj=BytesIO(torrent.read()))
return torrent.read()
def print_search_results(mags, sizes, uploaded, local):
columns = int(os.popen('stty size', 'r').read().split()[1])
cur_color = 'zebra_0'
if local:
print('{:>4} {:{length}}'.format(
'LINK', 'NAME', length=columns - 8),
color='header')
else:
print('{:>4} {:>5} {:>5} {:>5} {:9} {:11} {:{length}}'.format(
'LINK', 'SEED', 'LEECH', 'RATIO',
'SIZE', 'UPLOAD', 'NAME', length=columns - 52),
color='header')
for m, magnet in enumerate(mags):
# 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('+', ' ')
if local:
line = '{:5} {:{length}}'
content = [m, 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]
# compute the S/L ratio (Higher is better)
try:
ratio = no_seeders / no_leechers
except ZeroDivisionError:
ratio = float('inf')
line = ('{:4} {:5} {:5} {:5.1f} {:5.1f}'
' {:3} {:<11} {:{length}}')
content = [m, 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 print_descriptions(chosen_links, mags, site, identifiers):
for link in chosen_links:
link = int(link)
path = '/torrent/%s/' % identifiers[link]
req = request.Request(site + path, headers=default_headers)
req.add_header('Accept-encoding', 'gzip')
f = request.urlopen(req, timeout=default_timeout)
if f.info().get('Content-Encoding') == 'gzip':
f = gzip.GzipFile(fileobj=BytesIO(f.read()))
res = f.read().decode('utf-8')
name = re.search(r'dn=([^\&]*)', mags[link][0])
torrent_name = parse.unquote(name.group(1)).replace('+', ' ')
desc = re.search(r'<div class="nfo">\s*<pre>(.+?)(?=</pre>)',
res, re.DOTALL).group(1)
# Replace HTML links with markdown style versions
desc = re.sub(r'<a href="\s*([^"]+?)\s*"[^>]*>(\s*)([^<]+?)(\s*'
r')</a>', r'\2[\3](\1)\4', desc)
print('Description for "%s":' % torrent_name, color='zebra_1')
print(desc, color='zebra_0')
def print_file_lists(chosen_links, mags, site, identifiers):
for link in chosen_links:
path = '/ajax_details_filelist.php'
query = '?id=' + identifiers[int(link)]
req = request.Request(site + path + query, headers=default_headers)
req.add_header('Accept-encoding', 'gzip')
f = request.urlopen(req, timeout=default_timeout)
if f.info().get('Content-Encoding') == 'gzip':
f = gzip.GzipFile(fileobj=BytesIO(f.read()))
res = f.read().decode('utf-8').replace('&nbsp;', ' ')
files = re.findall(r'<td align="left">\s*([^<]+?)\s*</td><td ali'
r'gn="right">\s*([^<]+?)\s*</tr>', res)
name = re.search(r'dn=([^\&]*)', mags[int(link)][0])
torrent_name = parse.unquote(name.group(1)).replace('+', ' ')
print('Files in "%s":' % torrent_name, color='zebra_1')
cur_color = 'zebra_0'
for f in files:
print('{0[0]:>11} {0[1]}'.format(f), color=cur_color)
cur_color = 'zebra_0' if (cur_color == 'zebra_1') else 'zebra_1'
def save_torrents(chosen_links, mags, folder):
for link in chosen_links:
magnet = mags[int(link)][0]
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)
file = os.path.join(folder, torrent_name + '.torrent')
try:
torrent = get_torrent(info_hash)
except HTTPError:
print('There is no cached file for this torrent :(', color='ERROR')
else:
open(file,'wb').write(torrent)
print('Saved {:X} in {}'.format(info_hash, file))
def save_magnets(chosen_links, mags, folder):
for link in chosen_links:
magnet = mags[int(link)][0]
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)
file = os.path.join(folder, torrent_name + '.magnet')
print('Saved {:X} in {}'.format(info_hash, file))
with open(file, 'w') as f:
f.write(magnet + '\n')
def main():
config = load_config()
parser = argparse.ArgumentParser(
description='finds and downloads torrents from the Pirate Bay')
parser.add_argument('-b', dest='browse',
action='store_true',
help='display in Browse mode')
parser.add_argument('search', metavar='search',
nargs='*', help='term to search for')
parser.add_argument('-c', dest='category', metavar='category',
help='specify a category to search', default='All')
parser.add_argument('-s', dest='sort', metavar='sort',
help='specify a sort option', default='SeedersDsc')
parser.add_argument('-R', dest='recent', action='store_true',
help='torrents uploaded in the last 48hours.'
'*ignored in searches*')
parser.add_argument('-l', dest='list_categories',
action='store_true',
help='list categories')
parser.add_argument('--list_sorts', dest='list_sorts',
action='store_true',
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,
help='the number of pages to fetch '
"(doesn't work with --local)")
parser.add_argument('-0', dest='first',
action='store_true',
help='choose the top result')
parser.add_argument('-a', '--download-all',
action='store_true',
help='download all results')
parser.add_argument('-t', '--transmission',
action='store_true',
help='open magnets with transmission-remote')
parser.add_argument('-P', '--port', dest='port',
help='transmission-remote rpc port. default is 9091')
parser.add_argument('-C', '--custom', dest='command',
help='open magnets with a custom command'
' (%%s will be replaced with the url)')
parser.add_argument('-M', '--save-magnets',
action='store_true',
help='save magnets links as files')
parser.add_argument('-T', '--save-torrents',
action='store_true',
help='save torrent files')
parser.add_argument('-S', '--save-directory',
type=str, metavar='DIRECTORY',
help='directory where to save downloaded files'
' (if none is given $PWD will be used)')
parser.add_argument('--disable-colors', dest='color',
action='store_false',
help='disable colored output')
args = parser.parse_args()
if (config.getboolean('Misc', 'colors') and not args.color
or not config.getboolean('Misc', 'colors')):
global colored_output
colored_output = False
if args.save_directory:
config.set('Save', 'directory', args.save_directory)
transmission_command = ['transmission-remote']
if args.port:
transmission_command.append(args.port)
if args.transmission or config.getboolean('Misc', 'transmission'):
ret = subprocess.call(transmission_command + ['-l'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
if ret != 0:
print('Transmission is not running.')
return
if args.list_categories:
cur_color = 'zebra_0'
for key, value in sorted(categories.items()) :
cur_color = 'zebra_0' if cur_color == 'zebra_1' else 'zebra_1'
print(str(value), '\t', key, sep='', color=cur_color)
return
if args.list_sorts:
cur_color = 'zebra_0'
for key, value in sorted(sorts.items()):
cur_color = 'zebra_0' if cur_color == 'zebra_1' else 'zebra_1'
print(str(value), '\t', key, sep='', color=cur_color)
return
if args.database or config.getboolean('LocalDB', 'enabled'):
if args.database:
path = args.database
else:
path = config.get('LocalDB', 'path')
mags = local(path, args.search)
sizes, uploaded = [], []
else:
mags, mirrors = [], {'https://thepiratebay.se'}
try:
req = request.Request('https://proxybay.co/list.txt',
headers=default_headers)
f = request.urlopen(req, timeout=default_timeout)
except IOError:
print('Could not fetch additional mirrors', color='WARN')
else:
if f.getcode() != 200:
raise IOError('The proxy bay responded with an error.')
mirrors = mirrors.union([i.decode('utf-8').strip()
for i in f.readlines()][3:])
for mirror in mirrors:
try:
print('Trying', mirror, end='... ')
mags, sizes, uploaded, identifiers = remote(args, mirror)
except (URLError, IOError, ValueError, timeout):
print('Failed', color='WARN')
else:
site = mirror
print('Ok', color='alt')
break
else:
print('No available mirrors :(', color='WARN')
return
if not mags:
print('No results')
return
print_search_results(mags, sizes, uploaded, local=args.database)
if args.first:
print('Choosing first result')
choices = [0]
elif args.download_all:
print('Downloading all results')
choices = range(len(mags))
else:
# New input loop to support different link options
while True:
print("\nSelect links (Type 'h' for more options"
", 'q' to quit)", end='\b', color='alt')
try:
l=input(': ')
except KeyboardInterrupt :
print('\nCancelled.')
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 = []
for elem in parsed_input: # loop will generate a list of lists
left, sep, right = elem.partition('-')
if right:
choices.append(list(range(int(left), int(right) + 1)))
else:
choices.append([int(left)])
choices = sum(choices, []) # flatten list
choices = [str(elem) for elem in choices] # the current code stores the choices as strings instead of ints. not sure if necessary
# Act on option, if supplied
print('')
if code == 'h':
print('Options:',
'<links>: Download selected torrents',
'[m<links>]: Save magnets as files',
'[t<links>]: Save .torrent files',
'[d<links>]: Get descriptions',
'[f<links>]: Get files',
'[p] Print search results',
'[q] Quit', sep='\n')
elif code == 'q':
print('Bye.', color='alt')
return
elif code == 'd':
print_descriptions(choices, mags, site, identifiers)
elif code == 'f':
print_file_lists(choices, mags, site, identifiers)
elif code == 'p':
print_search_results(mags, sizes, uploaded)
elif code == 'm':
save_magnets(choices, mags,
config.get('Save', 'directory'))
elif code == 't':
save_torrents(choices, mags,
config.get('Save', 'directory'))
elif not l:
print('No links entered!', color='WARN')
else:
break
except Exception as e:
print('Exception:', e, color='ERROR')
choices = ()
save_to_file = False
if args.save_magnets or config.getboolean('Save', 'magnets'):
print('Saving selected magnets...')
save_magnets(choices, mags, config.get('Save', 'directory'))
save_to_file = True
if args.save_torrents or config.getboolean('Save', 'torrents'):
print('Saving selected torrents...')
save_torrents(choices, mags, config.get('Save', 'directory'))
save_to_file = True
if save_to_file:
return
for choice in choices:
url = mags[int(choice)][0]
if args.transmission or config.getboolean('Misc', 'transmission'):
subprocess.call(transmission_command + ['--add', url], shell=False)
elif args.command or config.get('Misc', 'openCommand'):
command = config.get('Misc', 'openCommand')
if args.command:
command = args.command
subprocess.call(parse_cmd(command, url), shell=False)
else:
webbrowser.open(url)
if args.transmission or config.getboolean('Misc', 'transmission'):
subprocess.call(transmission_command + ['-l'])
if __name__ == '__main__':
main()

0
pirate/__init__.py Normal file
View File

8
pirate/data.py Normal file
View File

@ -0,0 +1,8 @@
import json
categories = json.load(open('data/categories.json'))
sorts = json.load(open('data/sorts.json'))
default_headers = {'User-Agent': 'pirate get'}
default_timeout = 10
colored_output = True

41
pirate/local.py Normal file
View File

@ -0,0 +1,41 @@
import urllib.parse as parse
import html.parser as parser
class BayParser(parser.HTMLParser):
title = ''
q = ''
state = 'looking'
results = []
def __init__(self, q):
super().__init__(self)
self.q = q.lower()
def handle_starttag(self, tag, attrs):
if tag == 'title':
self.state = 'title'
if tag == 'magnet' and self.state == 'matched':
self.state = 'magnet'
def handle_data(self, data):
if self.state == 'title':
if data.lower().find(self.q) != -1:
self.title = data
self.state = 'matched'
else:
self.state = 'looking'
if self.state == 'magnet':
self.results.append([
'magnet:?xt=urn:btih:' +
parse.quote(data) +
'&dn=' +
parse.quote(self.title), '?', '?'])
self.state = 'looking'
def search(db, terms):
xml = open(db).readlines()
parser = BayParser(' '.join(terms))
parser.feed(''.join(xml))
return parser.results

323
pirate/pirate.py Executable file
View File

@ -0,0 +1,323 @@
import re
import os
import argparse
import subprocess
import configparser
import socket
import urllib.request as request
import urllib.error
import webbrowser
import pirate.data
import pirate.torrent
import pirate.local
import pirate.print
from os.path import expanduser, expandvars
from pirate.print import print
def load_config():
config = configparser.ConfigParser()
# default options
config.add_section('Save')
config.set('Save', 'magnets', 'false')
config.set('Save', 'torrents', 'false')
config.set('Save', 'directory', os.getcwd())
config.add_section('LocalDB')
config.set('LocalDB', 'enabled', 'false')
config.set('LocalDB', 'path', expanduser('~/downloads/pirate-get/db'))
config.add_section('Misc')
config.set('Misc', 'openCommand', '')
config.set('Misc', 'transmission', 'false')
config.set('Misc', 'colors', 'true')
# user-defined config files
main = expandvars('$XDG_CONFIG_HOME/pirate-get')
alt = expanduser('~/.config/pirate-get')
# read config file
config.read([main] if os.path.isfile(main) else [alt])
# expand env variables
directory = expanduser(expandvars(config.get('Save', 'Directory')))
path = expanduser(expandvars(config.get('LocalDB', 'path')))
config.set('Save', 'Directory', directory)
config.set('LocalDB', 'path', path)
return config
def parse_cmd(cmd, url):
cmd_args_regex = r'''(('[^']*'|"[^"]*"|(\\\s|[^\s])+)+ *)'''
ret = re.findall(cmd_args_regex, cmd)
ret = [i[0].strip().replace('%s', url) for i in ret]
ret_no_quotes = []
for item in ret:
if ((item[0] == "'" and item[-1] == "'") or
(item[0] == '"' and item[-1] == '"')):
ret_no_quotes.append(item[1:-1])
else:
ret_no_quotes.append(item)
return ret_no_quotes
def main():
config = load_config()
parser = argparse.ArgumentParser(
description='finds and downloads torrents from the Pirate Bay')
parser.add_argument('-b', dest='browse',
action='store_true',
help='display in Browse mode')
parser.add_argument('search', metavar='search',
nargs='*', help='term to search for')
parser.add_argument('-c', dest='category', metavar='category',
help='specify a category to search', default='All')
parser.add_argument('-s', dest='sort', metavar='sort',
help='specify a sort option', default='SeedersDsc')
parser.add_argument('-R', dest='recent', action='store_true',
help='torrents uploaded in the last 48hours.'
'*ignored in searches*')
parser.add_argument('-l', dest='list_categories',
action='store_true',
help='list categories')
parser.add_argument('--list_sorts', dest='list_sorts',
action='store_true',
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,
help='the number of pages to fetch '
"(doesn't work with --local)")
parser.add_argument('-0', dest='first',
action='store_true',
help='choose the top result')
parser.add_argument('-a', '--download-all',
action='store_true',
help='download all results')
parser.add_argument('-t', '--transmission',
action='store_true',
help='open magnets with transmission-remote')
parser.add_argument('-P', '--port', dest='port',
help='transmission-remote rpc port. default is 9091')
parser.add_argument('-C', '--custom', dest='command',
help='open magnets with a custom command'
' (%%s will be replaced with the url)')
parser.add_argument('-M', '--save-magnets',
action='store_true',
help='save magnets links as files')
parser.add_argument('-T', '--save-torrents',
action='store_true',
help='save torrent files')
parser.add_argument('-S', '--save-directory',
type=str, metavar='DIRECTORY',
help='directory where to save downloaded files'
' (if none is given $PWD will be used)')
parser.add_argument('--disable-colors', dest='color',
action='store_false',
help='disable colored output')
args = parser.parse_args()
if (config.getboolean('Misc', 'colors') and not args.color
or not config.getboolean('Misc', 'colors')):
pirate.data.colored_output = False
if args.save_directory:
config.set('Save', 'directory', args.save_directory)
transmission_command = ['transmission-remote']
if args.port:
transmission_command.append(args.port)
if args.transmission or config.getboolean('Misc', 'transmission'):
ret = subprocess.call(transmission_command + ['-l'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
if ret != 0:
print('Transmission is not running.')
return
if args.list_categories:
cur_color = 'zebra_0'
for key, value in sorted(pirate.data.categories.items()):
cur_color = 'zebra_0' if cur_color == 'zebra_1' else 'zebra_1'
print(str(value), '\t', key, sep='', color=cur_color)
return
if args.list_sorts:
cur_color = 'zebra_0'
for key, value in sorted(pirate.data.sorts.items()):
cur_color = 'zebra_0' if cur_color == 'zebra_1' else 'zebra_1'
print(str(value), '\t', key, sep='', color=cur_color)
return
if args.database or config.getboolean('LocalDB', 'enabled'):
if args.database:
path = args.database
else:
path = config.get('LocalDB', 'path')
mags = pirate.local.search(path, args.search)
sizes, uploaded = [], []
else:
mags, mirrors = [], {'https://thepiratebay.mn'}
try:
req = request.Request('https://proxybay.co/list.txt',
headers=pirate.data.default_headers)
f = request.urlopen(req, timeout=pirate.data.default_timeout)
except IOError:
print('Could not fetch additional mirrors', color='WARN')
else:
if f.getcode() != 200:
raise IOError('The proxy bay responded with an error.')
mirrors = mirrors.union([i.decode('utf-8').strip()
for i in f.readlines()][3:])
# This mirror messes up links in the search results page - you need to load a second page to get the magnet link
mirrors.discard('https://thebay.tv')
for mirror in mirrors:
try:
print('Trying', mirror, end='... ')
mags, sizes, uploaded, ids = pirate.torrent.remote(args,
mirror)
except (urllib.error.URLError, socket.timeout,
IOError, ValueError):
print('Failed', color='WARN')
else:
site = mirror
print('Ok', color='alt')
break
else:
print('No available mirrors :(', color='WARN')
return
if not mags:
print('No results')
return
pirate.print.search_results(mags, sizes, uploaded, local=args.database)
if args.first:
print('Choosing first result')
choices = [0]
elif args.download_all:
print('Downloading all results')
choices = range(len(mags))
else:
# New input loop to support different link options
while True:
print("\nSelect links (Type 'h' for more options"
", 'q' to quit)", end='\b', color='alt')
try:
l = input(': ')
except KeyboardInterrupt:
print('\nCancelled.')
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 = []
for elem in parsed_input: # loop will generate a list of lists
left, sep, right = elem.partition('-')
if right:
choices.append(list(range(int(left), int(right) + 1)))
else:
choices.append([int(left)])
choices = sum(choices, []) # flatten list
choices = [str(elem) for elem in choices] # the current code stores the choices as strings instead of ints. not sure if necessary
# Act on option, if supplied
print('')
if code == 'h':
print('Options:',
'<links>: Download selected torrents',
'[m<links>]: Save magnets as files',
'[t<links>]: Save .torrent files',
'[d<links>]: Get descriptions',
'[f<links>]: Get files',
'[p] Print search results',
'[q] Quit', sep='\n')
elif code == 'q':
print('Bye.', color='alt')
return
elif code == 'd':
pirate.print.descriptions(choices, mags, site, ids)
elif code == 'f':
pirate.print.file_lists(choices, mags, site, ids)
elif code == 'p':
pirate.print.search_results(mags, sizes, uploaded)
elif code == 'm':
pirate.torrent.save_magnets(choices, mags, config.get(
'Save', 'directory'))
elif code == 't':
pirate.torrent.save_torrents(choices, mags, config.get(
'Save', 'directory'))
elif not l:
print('No links entered!', color='WARN')
else:
break
except Exception as e:
print('Exception:', e, color='ERROR')
choices = ()
save_to_file = False
if args.save_magnets or config.getboolean('Save', 'magnets'):
print('Saving selected magnets...')
pirate.torrent.save_magnets(choices, mags, 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(
'Save', 'directory'))
save_to_file = True
if save_to_file:
return
for choice in choices:
url = mags[int(choice)][0]
if args.transmission or config.getboolean('Misc', 'transmission'):
subprocess.call(transmission_command + ['--add', url])
elif args.command or config.get('Misc', 'openCommand'):
command = config.get('Misc', 'openCommand')
if args.command:
command = args.command
subprocess.call(parse_cmd(command, url))
else:
webbrowser.open(url)
if args.transmission or config.getboolean('Misc', 'transmission'):
subprocess.call(transmission_command + ['-l'])
if __name__ == '__main__':
main()

127
pirate/print.py Normal file
View File

@ -0,0 +1,127 @@
import builtins
import re
import os
import gzip
import colorama
import urllib.parse as parse
import urllib.request as request
from io import BytesIO
import pirate.data
def print(*args, **kwargs):
if kwargs.get('color', False) and pirate.data.colored_output:
colorama.init()
color_dict = {
'default': '',
'header': colorama.Back.BLACK + colorama.Fore.WHITE,
'alt': colorama.Fore.YELLOW,
'zebra_0': '',
'zebra_1': colorama.Fore.BLUE,
'WARN': colorama.Fore.MAGENTA,
'ERROR': colorama.Fore.RED}
c = color_dict[kwargs.pop('color')]
args = (c + args[0],) + args[1:] + (colorama.Style.RESET_ALL,)
kwargs.pop('color', None)
return builtins.print(*args, **kwargs)
else:
kwargs.pop('color', None)
return builtins.print(*args, **kwargs)
def search_results(mags, sizes, uploaded, local):
columns = int(os.popen('stty size', 'r').read().split()[1])
cur_color = 'zebra_0'
if local:
print('{:>4} {:{length}}'.format(
'LINK', 'NAME', length=columns - 8),
color='header')
else:
print('{:>4} {:>5} {:>5} {:>5} {:9} {:11} {:{length}}'.format(
'LINK', 'SEED', 'LEECH', 'RATIO',
'SIZE', 'UPLOAD', 'NAME', length=columns - 52),
color='header')
for m, magnet in enumerate(mags):
# 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('+', ' ')
if local:
line = '{:5} {:{length}}'
content = [m, 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]
# compute the S/L ratio (Higher is better)
try:
ratio = no_seeders / no_leechers
except ZeroDivisionError:
ratio = float('inf')
line = ('{:4} {:5} {:5} {:5.1f} {:5.1f}'
' {:3} {:<11} {:{length}}')
content = [m, 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):
for link in chosen_links:
link = int(link)
path = '/torrent/%s/' % identifiers[link]
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)
if f.info().get('Content-Encoding') == 'gzip':
f = gzip.GzipFile(fileobj=BytesIO(f.read()))
res = f.read().decode('utf-8')
name = re.search(r'dn=([^\&]*)', mags[link][0])
torrent_name = parse.unquote(name.group(1)).replace('+', ' ')
desc = re.search(r'<div class="nfo">\s*<pre>(.+?)(?=</pre>)',
res, re.DOTALL).group(1)
# Replace HTML links with markdown style versions
desc = re.sub(r'<a href="\s*([^"]+?)\s*"[^>]*>(\s*)([^<]+?)(\s*'
r')</a>', r'\2[\3](\1)\4', desc)
print('Description for "%s":' % torrent_name, color='zebra_1')
print(desc, color='zebra_0')
def file_lists(chosen_links, mags, site, identifiers):
for link in chosen_links:
path = '/ajax_details_filelist.php'
query = '?id=' + identifiers[int(link)]
req = request.Request(site + path + query,
headers=pirate.data.default_headers)
req.add_header('Accept-encoding', 'gzip')
f = request.urlopen(req, timeout=pirate.data.default_timeout)
if f.info().get('Content-Encoding') == 'gzip':
f = gzip.GzipFile(fileobj=BytesIO(f.read()))
res = f.read().decode('utf-8').replace('&nbsp;', ' ')
files = re.findall(r'<td align="left">\s*([^<]+?)\s*</td><td ali'
r'gn="right">\s*([^<]+?)\s*</tr>', res)
name = re.search(r'dn=([^\&]*)', mags[int(link)][0])
torrent_name = parse.unquote(name.group(1)).replace('+', ' ')
print('Files in "%s":' % torrent_name, color='zebra_1')
cur_color = 'zebra_0'
for f in files:
print('{0[0]:>11} {0[1]}'.format(f), color=cur_color)
cur_color = 'zebra_0' if (cur_color == 'zebra_1') else 'zebra_1'

155
pirate/torrent.py Normal file
View File

@ -0,0 +1,155 @@
import re
import sys
import gzip
import urllib.request as request
import urllib.parse as parse
import urllib.error
import pirate.data
from io import BytesIO
#todo: redo this with html parser instead of regex
def remote(args, 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))
req = request.Request(mirror + path,
headers=pirate.data.default_headers)
req.add_header('Accept-encoding', 'gzip')
f = request.urlopen(req, timeout=pirate.data.default_timeout)
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=[^"]*)|<td align="right">'
r'([^<]+)</td>', 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.')
# get sizes as well and substitute the &nbsp; character
sizes.extend([match.replace('&nbsp;', ' ').split()
for match in re.findall(r'(?<=Size )[0-9.]'
r'+\&nbsp\;[KMGT]*[i ]*B', res)])
uploaded.extend([match.replace('&nbsp;', ' ')
for match in re.findall(r'(?<=Uploaded )'
r'.+(?=\, Size)',res)])
identifiers.extend([match.replace('&nbsp;', ' ')
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 :
print('\nCancelled.')
sys.exit(0)
# return the sizes in a spearate list
return res_l, sizes, uploaded, identifiers
def get_torrent(info_hash):
url = 'http://torcache.net/torrent/{:X}.torrent'
req = request.Request(url.format(info_hash),
headers=pirate.data.default_headers)
req.add_header('Accept-encoding', 'gzip')
torrent = request.urlopen(req, timeout=pirate.data.default_timeout)
if torrent.info().get('Content-Encoding') == 'gzip':
torrent = gzip.GzipFile(fileobj=BytesIO(torrent.read()))
return torrent.read()
def save_torrents(chosen_links, mags, folder):
for link in chosen_links:
magnet = mags[int(link)][0]
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)
file = os.path.join(folder, torrent_name + '.torrent')
try:
torrent = get_torrent(info_hash)
except urllib.error.HTTPError:
print('There is no cached file for this torrent :(', color='ERROR')
else:
open(file,'wb').write(torrent)
print('Saved {:X} in {}'.format(info_hash, file))
def save_magnets(chosen_links, mags, folder):
for link in chosen_links:
magnet = mags[int(link)][0]
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)
file = os.path.join(folder, torrent_name + '.magnet')
print('Saved {:X} in {}'.format(info_hash, file))
with open(file, 'w') as f:
f.write(magnet + '\n')

2
requirements-test.txt Normal file
View File

@ -0,0 +1,2 @@
-r requirements.txt
coverage

0
requirements.txt Normal file
View File

23
setup.py Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
setup(name='pirate-get',
version='0.2.5',
description='A command line interface for The Pirate Bay',
url='https://github.com/vikstrous/pirate-get',
author='vikstrous',
author_email='me@viktorstanchev.com',
license='GPL',
packages=find_packages(),
entry_points={
'console_scripts': ['pirate-get = pirate.pirate:main']
},
install_requires=['colorama'],
keywords=['torrent', 'magnet', 'download', 'tpb', 'client'],
classifiers=[
'Topic :: Utilities',
'Topic :: Terminals',
'Topic :: System :: Networking',
'Programming Language :: Python :: 3 :: Only',
'License :: OSI Approved :: GNU General Public License (GPL)',
])

0
tests/__init__.py Normal file
View File

168
tests/rich.xml Normal file
View File

@ -0,0 +1,168 @@
<torrent>
<id>3211594</id>
<title>High.Chaparall.S02E02.PDTV.XViD.SWEDiSH-HuBBaTiX</title>
<magnet>b03c8641415d3a0fc7077f5bf567634442989a74</magnet>
<size>375299009</size>
<seeders>1</seeders>
<leechers>0</leechers>
<quality><up>0</up><down>0</down></quality>
<uploaded>2004-03-25 23:08:00</uploaded>
<nfo>Andra avsnittet på säsong två av High Chaparall.</nfo>
<comments>
<comment><when>2004-04-05 18:56</when><what>kan nån seeda första avsnittet</what></comment>
<comment><when>2004-05-03 19:18</when><what>Ja snälla ta och seeda saknar 0,9%.</what></comment>
<comment><when>2004-05-25 18:32</when><what>Snälla, kan någon seeda?&lt;br /&gt;
1 % kvar :S</what></comment>
<comment><when>2004-05-26 11:28</when><what>asså har legat på 99% nu i 2 veckor, dryyyyyyyygt!!</what></comment>
<comment><when>2004-05-27 21:14</when><what>Legat på 99 % ett bra tag nu jag också, vore tacksam om någon kunde seeda</what></comment>
<comment><when>2006-06-22 23:03</when><what>Er det helt nye afsnit? :D</what></comment>
<comment><when>2006-06-25 17:03</when><what>Wow, the piratebay has really gone to shit, What happened?</what></comment>
<comment><when>2006-06-26 07:22</when><what>wth is going on</what></comment>
<comment><when>2006-06-30 07:44</when><what>INFO av avsnit ?????</what></comment>
<comment><when>2006-06-30 07:44</when><what>avsnitt</what></comment>
<comment><when>2006-08-22 02:10</when><what>Uri Geller?&lt;br /&gt;</what></comment>
<comment><when>2008-10-04 18:14</when><what>Form, I was going to say that :(</what></comment>
<comment><when>2009-01-28 20:49</when><what>lol first torrent on TPB EVER.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
wow.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
u think he could have picked a better name than kbdcb lolololol</what></comment>
<comment><when>2009-03-03 22:59</when><what>It's the oldest TV Show torrent that hasn't been deleted,&lt;br /&gt;
&lt;br /&gt;
yet</what></comment>
<comment><when>2010-04-11 08:26</when><what>wow i found that this is the first torrent ever to be uploaded to tpb</what></comment>
<comment><when>2010-07-13 13:59</when><what>oldest torrent evur</what></comment>
<comment><when>2010-08-04 16:54</when><what>precisely my thought... :)</what></comment>
<comment><when>2011-01-02 18:57</when><what>Well I have a torrent that uploaded 09-02 2004.&lt;br /&gt;
And it´s still active :D&lt;br /&gt;
7 Years ftw guys!!</what></comment>
<comment><when>2011-11-26 02:22</when><what>"...Querido diario íntimo: mi corazón estalló de emoción al descubrir el torrent más viejo en 'The Pirate Bay'. Estoy feliz por comentar esta publicación y ser parte de la historia de TPB. Es como escalar el Everest. Sinceramente, gracias...".&lt;br /&gt;
&lt;br /&gt;
"... Dear diary, my heart burst of excitement to discover the oldest torrent in The Pirate Bay '. I am happy to comment on this book and be part of the history of TPB. It's like climbing Everest. Sincerely, thanks ...".&lt;br /&gt;
&lt;br /&gt;
"... Kära dagbok, mitt hjärta brast av spänning för att upptäcka den äldsta torrent i The Pirate Bay". Jag är glad att kommentera denna bok och vara en del av historien om TPB. Det är som att klättra Everest. Vänliga hälsningar, tack ...".&lt;br /&gt;
&lt;br /&gt;
"... Liebes Tagebuch, mein Herz brach der Aufregung um die älteste torrent in The Pirate Bay" zu entdecken. Ich freue mich auf dieses Buch kommentieren und werden Sie Teil der Geschichte der TPB. Es ist wie Bergsteigen Everest. Mit freundlichen Grüßen, dank ...".</what></comment>
<comment><when>2012-01-09 11:04</when><what>just find this first torrent on TPB, from the help of manOtor m8, thanks 4 ur work kbdcb :)</what></comment>
</comments>
</torrent>
<torrent>
<id>3211609</id>
<title>School.Of.Rock.PROPER.DVDRip.XviD-DMT</title>
<magnet>a896f7155237fb27e2eaa06033b5796d7ae84a1d</magnet>
<size>739308799</size>
<seeders>0</seeders>
<leechers>2</leechers>
<quality><up>0</up><down>0</down></quality>
<uploaded>2004-03-26 09:35:17</uploaded>
<nfo>OrginalRelease</nfo>
<comments>
<comment><when>2004-04-06 06:24</when><what>hur fan öppnar jag filmen</what></comment>
<comment><when>2004-04-17 19:42</when><what>med winrar&lt;br /&gt;</what></comment>
<comment><when>2004-04-18 21:19</when><what>lite seg i början... men riktigt bra efter ett tag</what></comment>
<comment><when>2004-04-28 21:03</when><what>Seeda...</what></comment>
<comment><when>2004-05-05 12:30</when><what>Kan ingen seeda... Ligger på 97%... Skojj...</what></comment>
<comment><when>2004-05-08 09:57</when><what>hur fixar man filmen när den bara"darrar" bilden likson skakar hela tiden. Någon som vet??</what></comment>
<comment><when>2004-05-09 22:53</when><what>"hur fixar man filmen när den bara"darrar" bilden likson skakar hela tiden. Någon som vet?"&lt;br /&gt;
&lt;br /&gt;
precis samma sak för mig, försökt med alla codecs mm. fatar 0:an! någon som vet?</what></comment>
<comment><when>2004-05-10 11:11</when><what>Ni som lyckats med denna film kan väl höra av er och berätta hur ni gjort!</what></comment>
<comment><when>2004-06-07 00:51</when><what>SWESUB: &lt;a href="http://www.undertexter.se/index.php?p=subark&id=1148" rel="nofollow" target="_new"&gt;http://www.undertexter.se/index.php?p=subark&id=1148&lt;/a&gt;</what></comment>
<comment><when>2004-06-22 23:44</when><what>snälla seeda.</what></comment>
<comment><when>2004-08-04 08:15</when><what>Nån måste läea mig hur man lägger in subs!!&lt;br /&gt;
Och mitt nero (nyaste) bränner INTE avi filer.... :S&lt;br /&gt;
&lt;br /&gt;
:axe:</what></comment>
<comment><when>2004-09-02 22:04</when><what>mrmaniac å erikapa Haft samma problem...lösningen heter vlc media player!! spelar upp allt perfect...till och med filer som windows ej kan identifiera=) har tyvärr ej URL...men kolla google... lycka till!</what></comment>
<comment><when>2004-09-05 07:53</when><what>Om ni ska ha vlc, kika in på &lt;a href="http://www.videolan.org./vlc/" rel="nofollow" target="_new"&gt;http://www.videolan.org./vlc/&lt;/a&gt;</what></comment>
<comment><when>2004-09-06 21:37</when><what>Nero som inte bränner avi? Det låter ju asdumt. Hur kommer man på något sånt? Säkert någon jäkla anti-piratgrej. *grr*</what></comment>
<comment><when>2004-09-11 19:56</when><what>Fyfan så bra film, synd att man inte hade en gitarr att rocka med =(. SEVÄRD!</what></comment>
<comment><when>2004-09-29 05:59</when><what>Fin kvalite och bra ljud. Hoppas bara att den funkar på DVD-spelarn nu :)</what></comment>
<comment><when>2004-10-09 01:09</when><what>hmm.. kan inte alla dela med sig mera när dom seedar.. snålt :/</what></comment>
<comment><when>2004-12-14 16:33</when><what>kan inte alla seeda när dom laddar ner:/ eller???</what></comment>
<comment><when>2004-12-14 16:35</when><what>SEEDAAA DE GÅR TRÖÖÖÖGT LIGGER PÅ 20 kb/s!!!:@</what></comment>
<comment><when>2004-12-20 20:41</when><what>Kan inte någon seeda lite mer än 20 kbs</what></comment>
<comment><when>2004-12-21 14:27</when><what>vlc- player spiller bare filer / filmer i et par sekunder så stopper den . åssen fixer jg dette</what></comment>
<comment><when>2004-12-21 16:44</when><what>Kan inte någon Seeda alla ligger på 81.8% Finns det inte nåon som vill försöka hålla igång det här så det funkar bra.</what></comment>
<comment><when>2005-01-04 13:10</when><what>Seeda plz</what></comment>
<comment><when>2005-01-04 13:11</when><what>jag kan seeda sen men ja vill gärna ha filmen först&lt;br /&gt;</what></comment>
<comment><when>2006-06-22 17:23</when><what>Seeda! Fast på 99.6% Jag ska hjälpa till om jag får ner filmen!</what></comment>
<comment><when>2006-06-22 23:28</when><what>seeda förfan sitter på 99.0%&lt;br /&gt;
&lt;br /&gt;
ooooooooooooooorka</what></comment>
<comment><when>2006-06-23 09:19</when><what>Va faen, jag fattar inget. Allt på hela TPB står med bara 1 seed, inkl. YOP 100. Igår var hela TOP 100 annorlunda med jävla "irish drinking songs" å "simpsons" på första plats. Inget verkar va sig likt eftr. tillslaget. Vad har hänt?</what></comment>
<comment><when>2006-06-24 13:11</when><what>Please seed. I'm at 93.3%, and have been for three days now.&lt;br /&gt;
And also... piet00piet, please stop spamming your comments. It's annoying.</what></comment>
<comment><when>2006-06-27 20:07</when><what>why cant i download the movie?&lt;br /&gt;
&lt;br /&gt;
can i have a step by step on how to do it?</what></comment>
<comment><when>2006-06-29 16:37</when><what>Fan Vad LOL Filmen Var Upp o ner när man spela upp den</what></comment>
<comment><when>2006-07-03 13:25</when><what>This movie roxorz</what></comment>
<comment><when>2006-07-03 14:32</when><what>PirateBay used to be good, but something has chnaged, now it sucks! You can not tell how many Seeders there are or no one seems to be seeding.</what></comment>
<comment><when>2006-07-07 18:33</when><what>Tänkte bara påpeka att gula sidorna finns på internet... och att Pirates of the Carribean: Dead Man Chest finns på IsoHunt nu. &lt;br /&gt;
&lt;br /&gt;
Har inte en susning om kvaliteteten håller på att slanga den själv nu.</what></comment>
<comment><when>2006-07-08 17:42</when><what>this is a funny movie! Both for kids and adults.</what></comment>
<comment><when>2006-07-09 02:52</when><what>Good stuff!!!</what></comment>
<comment><when>2011-11-01 18:01</when><what>&lt; a href="&lt;a href="http://www.imdb.com/title/tt0332379/" rel="nofollow" target="_new"&gt;http://www.imdb.com/title/tt0332379/&lt;/a&gt;"&gt;&lt; IMG SRC = "&lt;a href="http://www.imdb.com/title/tt0332379/" rel="nofollow" target="_new"&gt;http://www.imdb.com/title/tt0332379/&lt;/a&gt;" &gt;&lt; / a &gt;</what></comment>
<comment><when>2011-11-01 18:04</when><what>The School Of Rock</what></comment>
<comment><when>2011-11-01 18:29</when><what>&lt;a href="http://www.imdb.com/title/tt0332379/" rel="nofollow" target="_new"&gt;http://www.imdb.com/title/tt0332379/&lt;/a&gt;</what></comment>
<comment><when>2011-11-01 18:54</when><what>&lt; IMG SRC = "&lt;a href="http://www.imdb.com/media/rm3808337152/tt0332379" rel="nofollow" target="_new"&gt;http://www.imdb.com/media/rm3808337152/tt0332379&lt;/a&gt;" &gt;&lt; / a &gt;</what></comment>
<comment><when>2011-11-01 19:05</when><what>&lt;a href="LINKhttp://www.imdb.com/title/tt0332379/" rel="nofollow" target="_new"&gt;LINKhttp://www.imdb.com/title/tt0332379/&lt;/a&gt;</what></comment>
<comment><when>2011-11-01 19:24</when><what>LINK;&lt;a href="http://www.imdb.com/title/tt0332379/" rel="nofollow" target="_new"&gt;http://www.imdb.com/title/tt0332379/&lt;/a&gt;</what></comment>
<comment><when>2011-11-01 20:01</when><what>&lt;a href="http://www.imdb.com/media/rm3808337152/tt0332379" rel="nofollow" target="_new"&gt;http://www.imdb.com/media/rm3808337152/tt0332379&lt;/a&gt;</what></comment>
<comment><when>2011-11-01 20:36</when><what></what></comment>
</comments>
</torrent>
<torrent>
<id>3211623</id>
<title>Gyllene Tider-Samtliga Hits-SE-2004-WLM</title>
<magnet>3ebb7aa97076cac0ac1b0812f5e16cf46d5daf41</magnet>
<size>127185941</size>
<seeders>7</seeders>
<leechers>1</leechers>
<quality><up>1</up><down>0</down></quality>
<uploaded>2004-03-29 09:00:10</uploaded>
<nfo>Tanka på</nfo>
<comments>
<comment><when>2004-05-16 06:59</when><what>Seeda plz!&lt;br /&gt;
Solen skiner och det GT är ett måste :P</what></comment>
<comment><when>2004-05-20 15:43</when><what>seeda lite te..... plezzz måste ha den här samlingen råkade radera den innan</what></comment>
<comment><when>2004-05-31 11:43</when><what>kan nån jävel seeda?!?!&lt;br /&gt;</what></comment>
<comment><when>2004-05-31 18:11</when><what>ligger på 87,6% varför seedar ingen?!?!?!?!?!??!</what></comment>
<comment><when>2004-06-02 07:49</when><what>Jag såg att det behövdes någon som seedar. Håll till godo.</what></comment>
<comment><when>2004-06-02 16:45</when><what>tack... har väntat flera dar för att få ner den här</what></comment>
<comment><when>2004-06-13 08:07</when><what>Fan vaa nice!</what></comment>
<comment><when>2004-06-25 18:28</when><what>Varför kan jag inte koppla upp mig mot ngn peer!? :evil:&lt;br /&gt;
&lt;br /&gt;
Varenda annan jäkla torrent funkar men inte denna :'(</what></comment>
<comment><when>2004-08-12 12:35</when><what>TackaR! Detta har jag letat efter</what></comment>
<comment><when>2004-08-19 11:30</when><what>uh, men förfan, reseeda, så håller jag igång den i ett par veckor!&lt;br /&gt;
&lt;br /&gt;
orka ladda om när man bara har 10% kvar...</what></comment>
<comment><when>2005-04-02 05:23</when><what>seeda tack!</what></comment>
<comment><when>2006-06-23 17:02</when><what>We need seeders. I'm stuck at 99.7%. Is there anyone who could seed this?</what></comment>
<comment><when>2006-06-24 22:49</when><what>Sitter med på 99,7%.</what></comment>
<comment><when>2006-06-26 22:32</when><what>Seeda för helvete!</what></comment>
<comment><when>2006-06-26 22:34</when><what>6 stycken peers - Alla har 99,7% - Hooray!</what></comment>
<comment><when>2006-06-29 22:35</when><what>när jag trycker: download this torrent, så kommer den upp i typ en halv sekund, sen försvinner den. &lt;br /&gt;
vad gör jag för fel?</what></comment>
<comment><when>2006-06-30 14:25</when><what>SEEDA....vilken djävla dum kommentar!&lt;br /&gt;
&lt;br /&gt;
Klart att man seedar...iaf på riktiga trackers LOL&lt;br /&gt;
&lt;br /&gt;</what></comment>
<comment><when>2006-07-01 21:33</when><what>shyst !! jävligt bra ,,, tack m8 för att du ladda upp den xD.</what></comment>
<comment><when>2006-07-05 13:19</when><what>Bra att ni seedade nu&lt;br /&gt;
&lt;br /&gt;
Tack så hemskt mycket :)</what></comment>
<comment><when>2006-07-18 14:54</when><what>SNÄLLA SNÄLLA!! Seeda jag har stannat på 99% sen säkert en vecka...</what></comment>
<comment><when>2012-07-24 23:07</when><what>I sorted all of the music torrents by upload date and this came up as the oldest one. Unsurprising considering this is a Swedish website.&lt;br /&gt;
&lt;br /&gt;
Per Gessle rocks! Whooooo!</what></comment>
</comments>
</torrent>

15
tests/test_local.py Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import unittest
import pirate.local
import os
class TestLocal(unittest.TestCase):
def test_rich_xml(self):
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '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)
if __name__ == '__main__':
unittest.main()

View File

@ -1,2 +0,0 @@
#!/bin/sh
rm /usr/bin/pirate-get