├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py └── whatapi ├── __init__.py └── whatapi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | #Translations 30 | *.mo 31 | 32 | #Mr Developer 33 | .mr.developer.cfg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Isaac Zafuta 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | whatapi 2 | ======= 3 | 4 | This project is a simple wrapper around the What.cd AJAX API. Also compatible 5 | with what-like trackers such as pth/apollo. 6 | 7 | Available via PyPI via pip: 8 | 9 | :: 10 | 11 | # pip install whatapi 12 | 13 | 14 | Example usage: 15 | 16 | :: 17 | 18 | >>> import whatapi 19 | >>> apihandle = whatapi.WhatAPI(username='me', password='secret') 20 | >>> apihandle.request("browse", searchstr="Talulah Gosh") 21 | ... 22 | >>> apihandle.get_torrent(1234567) 23 | ... 24 | 25 | 26 | To use another tracker: 27 | 28 | :: 29 | 30 | >>> import whatapi 31 | >>> apihandle = whatapi.WhatAPI(username='me', password='secret', 32 | server='https://passtheheadphones.me') 33 | >>> apihandle.request("browse", searchstr="The Beatles") 34 | ... 35 | 36 | 37 | It's strongly recommended that your script implements saving/loading session cookies to prevent overloading the server. 38 | 39 | Example: 40 | 41 | :: 42 | 43 | >>> import whatapi 44 | >>> import cPickle as pickle 45 | >>> cookies = pickle.load(open('cookies.dat', 'rb')) 46 | >>> apihandle = whatapi.WhatAPI(username='me', password='me', cookies=cookies) 47 | ... 48 | >>> pickle.dump(apihandle.session.cookies, open('cookies.dat', 'wb')) 49 | 50 | API available at `Gwindow's API page `_ or via the JSON API page on What. 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.rst') as f: 4 | readme = f.read() 5 | 6 | with open('LICENSE') as f: 7 | license = f.read() 8 | 9 | setup( 10 | name='whatapi', 11 | version='0.2.0', 12 | description='What.cd API', 13 | long_description=readme, 14 | author='Isaac Zafuta', 15 | author_email='isaac@zafuta.com', 16 | url='https://github.com/isaaczafuta/whatapi', 17 | license=license, 18 | install_requires = [ 19 | "requests" 20 | ], 21 | packages=find_packages(exclude=('tests', 'docs')), 22 | package_data = { 23 | '': ['*.txt'] 24 | }, 25 | zip_safe=True 26 | ) 27 | -------------------------------------------------------------------------------- /whatapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .whatapi import WhatAPI 2 | 3 | __version__ = "0.2.0" 4 | -------------------------------------------------------------------------------- /whatapi/whatapi.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ConfigParser import ConfigParser 3 | except ImportError: 4 | from configparser import ConfigParser # py3k support 5 | import requests 6 | import time 7 | 8 | headers = { 9 | 'Content-type': 'application/x-www-form-urlencoded', 10 | 'Accept-Charset': 'utf-8', 11 | 'User-Agent': 'whatapi [isaaczafuta]' 12 | } 13 | 14 | class LoginException(Exception): 15 | pass 16 | 17 | 18 | class RequestException(Exception): 19 | pass 20 | 21 | 22 | class WhatAPI: 23 | def __init__(self, config_file=None, username=None, password=None, cookies=None, 24 | server="https://ssl.what.cd", throttler=None): 25 | self.session = requests.Session() 26 | self.session.headers = headers 27 | self.authkey = None 28 | self.passkey = None 29 | self.server = server 30 | self.throttler = Throttler(5, 10) if throttler is None else throttler 31 | if config_file: 32 | config = ConfigParser() 33 | config.read(config_file) 34 | self.username = config.get('login', 'username') 35 | self.password = config.get('login', 'password') 36 | else: 37 | self.username = username 38 | self.password = password 39 | if cookies: 40 | self.session.cookies = cookies 41 | try: 42 | self._auth() 43 | except RequestException: 44 | self._login() 45 | else: 46 | self._login() 47 | 48 | def _auth(self): 49 | '''Gets auth key from server''' 50 | accountinfo = self.request("index") 51 | self.authkey = accountinfo["response"]["authkey"] 52 | self.passkey = accountinfo["response"]["passkey"] 53 | 54 | def _login(self): 55 | '''Logs in user''' 56 | loginpage = self.server + '/login.php' 57 | data = {'username': self.username, 58 | 'password': self.password, 59 | 'keeplogged': 1, 60 | 'login': 'Login' 61 | } 62 | r = self.session.post(loginpage, data=data, allow_redirects=False) 63 | if r.status_code != 302: 64 | raise LoginException 65 | self._auth() 66 | 67 | def get_torrent(self, torrent_id, full_response=False): 68 | '''Downloads and returns the torrent file at torrent_id 69 | 70 | full_response: Returns the full response object (including headers) instead of a torrent file 71 | ''' 72 | torrentpage = self.server + '/torrents.php' 73 | params = {'action': 'download', 'id': torrent_id} 74 | if self.authkey: 75 | params['authkey'] = self.authkey 76 | params['torrent_pass'] = self.passkey 77 | if self.throttler: 78 | self.throttler.throttle_request() 79 | r = self.session.get(torrentpage, params=params, allow_redirects=False) 80 | if r.status_code == 200 and 'application/x-bittorrent' in r.headers['content-type']: 81 | return r if full_response else r.content 82 | return None 83 | 84 | def logout(self): 85 | '''Logs out user''' 86 | logoutpage = self.server + '/logout.php' 87 | params = {'auth': self.authkey} 88 | self.session.get(logoutpage, params=params, allow_redirects=False) 89 | 90 | def request(self, action, **kwargs): 91 | '''Makes an AJAX request at a given action page''' 92 | ajaxpage = self.server + '/ajax.php' 93 | params = {'action': action} 94 | if self.authkey: 95 | params['auth'] = self.authkey 96 | params.update(kwargs) 97 | 98 | if self.throttler: 99 | self.throttler.throttle_request() 100 | r = self.session.get(ajaxpage, params=params, allow_redirects=False) 101 | try: 102 | json_response = r.json() 103 | if json_response["status"] != "success": 104 | raise RequestException 105 | return json_response 106 | except ValueError: 107 | raise RequestException 108 | 109 | 110 | class Throttler(object): 111 | def __init__(self, num_requests=5, per_seconds=10): 112 | self.num_requests = num_requests 113 | self.per_seconds = per_seconds 114 | self.request_times = [] 115 | 116 | def throttle_request(self): 117 | request_time = time.time() 118 | if len(self.request_times) >= self.num_requests: 119 | sleep_time = self.per_seconds - (request_time - self.request_times[0]) 120 | if sleep_time > 0: 121 | time.sleep(sleep_time) 122 | self.request_times = self.request_times[1:] 123 | self.request_times.append(request_time) --------------------------------------------------------------------------------