├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── requirements.txt ├── setup.py ├── t411api ├── API.py ├── __init__.py ├── helpers.py └── version.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python related stuff 2 | __pycache__/ 3 | *.py[cod] 4 | env/ 5 | build/ 6 | dist/ 7 | *.egg-info/ 8 | pip-log.txt 9 | pip-delete-this-directory.txt 10 | 11 | # Dev related stuff 12 | .idea/ 13 | *~ 14 | *\#* 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.3' 4 | - '3.4' 5 | - '3.5' 6 | - '3.6' 7 | - nightly 8 | install: 9 | - pip install . 10 | script: nosetests 11 | # deploy: 12 | # provider: pypi 13 | # user: Xide 14 | # password: '' 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * Germain GAU wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | T411 API 2 | =================== 3 | 4 | [![PyPi](https://img.shields.io/pypi/v/t411api.svg)](https://pypi.python.org/pypi/t411api) 5 | [![Travis-CI](https://travis-ci.org/Xide/t411api.svg)](https://travis-ci.org/Xide/t411api) 6 | 7 | 8 | ---------- 9 | Installation 10 | ------------- 11 | 12 | ### Compatibility 13 | 14 | > This code is developped with Python 3.5 15 | > It sould however be compatible with Python >= 3.3 16 | 17 | ### Via pip 18 | 19 | ```sh 20 | # stable version 21 | pip3 install t411api 22 | # upstream version 23 | pip3 install git+https://git@github.com/Xide/t411api 24 | ``` 25 | 26 | ### The basic way 27 | 28 | ```sh 29 | git clone git@github.com:Xide/t411api.git && cd t411api 30 | python3 ./setup.py install 31 | ``` 32 | 33 | Use this method if you want the latest (maybe less stable) updates. 34 | 35 | ---------- 36 | Usage 37 | ------------- 38 | The API is exposed with the ```T411API``` class. 39 | 40 | API functions ( excepted ```connect``` will return a dictionnary containing the JSON datas returned by the API ( or raise upon unrecoverable error ) 41 | 42 | #### Connection 43 | 44 | The default API endpoint is ```https://api.t411.al```, it can be customized by passing the endpoint parameter to T411API's constructor as seen bellow. 45 | 46 | The T411 api only support plain-text authentication, the ```connect``` method will allow you to retreive your token ( mandatory to use the API ). 47 | 48 | ```python 49 | >>> import t411api 50 | >>> api = t411api.T411API() 51 | >>> # or 52 | >>> # api = t411api.T411API(endpoint='https://api.t411.al') 53 | >>> api.connect('username', 'password') 54 | ``` 55 | 56 | The API will raise a ```ServiceError``` or ```ConnectionError``` upon failure, details of the errors can be extracted from exception string. 57 | 58 | Otherwise, the ```api.token``` and ```api.uid``` fields will be set to the corresponding values. 59 | 60 | #### Search 61 | 62 | the ```T411API.search``` function provide search functionnality. 63 | 64 | ```python3 65 | >>> # T411API.search(query, **kwargs) 66 | >>> # Query is the mandatory string for your search 67 | >>> # kwargs are the T411 optionals arguments (ie: 'limit' 68 | >>> # argument that will fix the maximum number of responses ) 69 | >>> api.search('archlinux', limit=10) 70 | ``` 71 | 72 | #### Retrieving Top lists 73 | 74 | To fetch T411 tops, you can use the ```T411API.top``` method. 75 | The only parameter of the method is a string containing the name of the top you need. 76 | For more details about the API parameters, you can visit https://api.t411.al. 77 | At the time we are writing this documentation, choices are : 78 | 79 | - 100 80 | - day 81 | - week 82 | - month 83 | 84 | ```python 85 | >>> api.top('100') 86 | ``` 87 | 88 | #### Torrent details 89 | 90 | The ```T411API.details``` method retrieve details about a torrent 91 | ```python 92 | >>> # Retrieve details for the first 'archlinux' torrent 93 | >>> id = api.search('archlinux')['torrents'][0]['id'] 94 | >>> details = api.details(int(id)) 95 | ``` 96 | 97 | #### Downloading torrents 98 | 99 | The ```T411API.download``` method handle torrent downloading. 100 | She receive up to 3 parameters: 101 | 102 | 1. torrent_id: mandatory, the ID of the torrent you want to download 103 | 2. filename: optional, the name of the downloaded torrent file (torrent name by default) 104 | 3. base: optional: directory where the torrent file will be created (current directory by default) 105 | 106 | ```python 107 | >>> # Download the first 'archlinux' torrent 108 | >>> id = api.search('archlinux')['torrents'][0]['id'] 109 | >>> path = api.download(int(id)) 110 | ``` 111 | 112 | #### User informations 113 | ```T411API.user``` method return informations about an user. 114 | 115 | #### Bookmarks 116 | 117 | The bookmarks management is provided by 3 methods: 118 | 119 | 1. ```T411API.bookmarks``` 120 | This method return the current user bookmarks 121 | 2. ```T411API.add_bookmark``` 122 | 3. ```T411API.del_bookmark``` 123 | 124 | ---------- 125 | Exceptions 126 | ------------- 127 | Classes: 128 | ``` 129 | builtins.Exception(builtins.BaseException) 130 | APIError 131 | ConnectError 132 | ServiceError 133 | ``` 134 | 135 | **``` class APIError(builtins.Exception)```** 136 | > Exception thrown upon an unknow API error 137 | > Base class for every API exceptions 138 | 139 | **```class ConnectError(APIError)```** 140 | > Exception thrown when an error occur 141 | > on client side ( most likely receivable error ) 142 | 143 | **```class ServiceError(APIError)```** 144 | > Exception thrown when T411 service encounter an error 145 | 146 | ---------- 147 | FAQ 148 | ------------- 149 | 150 | Getting a ```ServiceError```: 151 | > This error is usually used when T411 is unreachable or encounter an unknown problem, check for [API status](http://www.downforeveryoneorjustme.com/api.t411.al) 152 | > If this service is up, please contact a project developer 153 | 154 | Getting an import error: 155 | > Some dependencies may have changed, try to run **pip install -r requirements.txt** to refresh dependency list 156 | 157 | ---------- 158 | Developers 159 | ------------- 160 | 161 | Hi, wanna join us ? Glad to hear that ! 162 | You can start browse the code, we are trying to comment a lot, especially for TODO's. 163 | Or you can try to improve one of theses : 164 | 165 | - Test coverage 166 | - Windows compatibility 167 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | __version__ = '1.0.0' 7 | exec(open('t411api/version.py').read()) 8 | 9 | setup( 10 | name='t411api', 11 | version=__version__, 12 | description='Lightweight API for T411 (french torrent website)', 13 | long_description='Documentation is available `here `_', 14 | url='https://github.com/Xide/t411api', 15 | author='Germain Gau', 16 | author_email='germain.gau@gmail.com', 17 | license='THE BEER-WARE LICENSE', 18 | packages=['t411api'], 19 | zip_safe=False, 20 | test_suite='tests', 21 | install_requires=[ 22 | 'requests>=2.13' 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Intended Audience :: Developers', 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.3", 29 | "Programming Language :: Python :: 3.4", 30 | "Programming Language :: Python :: 3.5", 31 | "Programming Language :: Python :: 3.6", 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | 'Programming Language :: Python', 34 | 'Operating System :: Unix' 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /t411api/API.py: -------------------------------------------------------------------------------- 1 | """ 2 | T411 API wrapper 3 | May throw APIError -> (ConnectError, ServiceError) 4 | """ 5 | 6 | import os 7 | import requests 8 | 9 | from t411api import helpers 10 | 11 | API_URL = 'https://api.t411.al' 12 | 13 | 14 | class APIError(Exception): 15 | """ 16 | Exception thrown upon an unknown API error 17 | Base class for every API exceptions 18 | """ 19 | pass 20 | 21 | 22 | class ConnectError(APIError): 23 | """ 24 | Exception thrown when an error occur 25 | on client side ( most likely receivable error ) 26 | """ 27 | pass 28 | 29 | 30 | class ServiceError(APIError): 31 | """ 32 | Exception thrown when T411 service encounter an error 33 | """ 34 | pass 35 | 36 | 37 | class T411API: 38 | def __init__(self, endpoint=API_URL): 39 | self.endpoint = endpoint 40 | self.token = None 41 | self.uid = None 42 | 43 | def connect(self, username: str, password: str): 44 | """ 45 | Connect to the T411 service 46 | May raise a ServiceError or a ConnectError 47 | :param username: T411 username 48 | :param password: user password (in plain text) 49 | :return: Nothing 50 | """ 51 | try: 52 | r = requests.post(self.endpoint + '/auth', data={ 53 | 'username': username, 54 | 'password': password, 55 | }) 56 | except Exception as e: 57 | # Broad clause, might change in the future, but not yet 58 | # useful 59 | raise ConnectError('Could not connect to API server: %s' % e) 60 | if r.status_code != 200: 61 | raise ServiceError('Unexpected HTTP code %d upon connection' 62 | % r.status_code) 63 | try: 64 | response = r.json() 65 | except ValueError: 66 | raise ServiceError('Unexpected non-JSON API response : %s' % r.content) 67 | 68 | if 'token' not in response.keys(): 69 | raise ConnectError('Unexpected T411 error : %s (%d)' 70 | % (response['error'], response['code'])) 71 | self.token = response['token'] 72 | self.uid = int(response['uid']) 73 | 74 | def _raw_query(self, path, params): 75 | """ 76 | Wraps API communication, with token and 77 | HTTP error code handling 78 | :param path: url to query 79 | :param params: http request parameters 80 | :return: Response object 81 | """ 82 | if not self.token: 83 | raise ConnectError('You must be logged in to use T411 API') 84 | 85 | if not params: 86 | params = {} 87 | 88 | r = requests.get(self.endpoint + path, params, 89 | headers={'Authorization': self.token}) 90 | 91 | if r.status_code != 200: 92 | raise ServiceError('Unexpected HTTP code %d upon connection' 93 | % r.status_code) 94 | return r 95 | 96 | def _query(self, path, params=None): 97 | """ 98 | Handle API response and errors 99 | :param path: 100 | :param params: 101 | :return: 102 | """ 103 | r = self._raw_query(path, params) 104 | try: 105 | response = r.json() 106 | except ValueError as e: 107 | raise ServiceError('Unexpected non-JSON response from T411: %s' 108 | % r.content if r else 'response is None') 109 | 110 | if isinstance(response, int): 111 | return response 112 | if 'error' in response: 113 | raise ServiceError('Unexpected T411 error : %s (%d)' 114 | % (response['error'], response['code'])) 115 | return response 116 | 117 | def details(self, torrent_id: int): 118 | """ 119 | Return details of the torrent 120 | :param torrent_id: id of the torrent (can be found with search) 121 | :return: 122 | """ 123 | return self._query('/torrents/details/%d' % torrent_id) 124 | 125 | def top(self, tp: str): 126 | """ 127 | return T411 top torrents 128 | :param tp: one of '100', 'day', 'week', 'month' 129 | :return: 130 | """ 131 | ctab = { 132 | '100': '/torrents/top/100', 133 | 'day': '/torrents/top/today', 134 | 'week': '/torrents/top/week', 135 | 'month': '/torrents/top/month' 136 | } 137 | if tp not in ctab.keys(): 138 | raise ValueError('Incorrect top command parameter') 139 | return self._query(ctab[tp]) 140 | 141 | def categories(self): 142 | return self._query('/categories/tree') 143 | 144 | def download(self, torrent_id: int, filename: str = '', base: str = ''): 145 | """ 146 | Download torrent on filesystem 147 | :param torrent_id: 148 | :param filename: torrent file name 149 | :param base: directory to put torrent into 150 | :return: 151 | """ 152 | if not filename: 153 | details = self.details(torrent_id) 154 | filename = helpers.sanitize(details['name']) 155 | if not base: 156 | base = os.getcwd() 157 | if not filename.endswith('.torrent'): 158 | filename += '.torrent' 159 | with open(os.path.join(base, filename), 'wb') as out: 160 | raw = self._raw_query('/torrents/download/%d' % torrent_id, {}) 161 | out.write(raw.content) 162 | return os.path.join(base, filename) 163 | 164 | def search(self, query: str, **kwargs): 165 | """ 166 | Search for a torrent, results are unordered 167 | :param query: 168 | :param kwargs: 169 | :return: 170 | """ 171 | 172 | params = { 173 | 'offset': 0, 174 | } 175 | 176 | params.update(kwargs) 177 | response = self._query('/torrents/search/' + query, params) 178 | return response 179 | 180 | def user(self, uid: int = None): 181 | """ 182 | Get stats about an user 183 | :param uid: user id 184 | :return: 185 | """ 186 | if not uid: 187 | uid = self.uid 188 | user = self._query('/users/profile/%d' % uid) 189 | if 'uid' not in user: 190 | user['uid'] = uid 191 | return user 192 | 193 | def bookmarks(self): 194 | """ 195 | retrieve list of user bookmarks 196 | :return: 197 | """ 198 | return self._query('/bookmarks') 199 | 200 | def add_bookmark(self, torrent_id: int): 201 | """ 202 | Add a new bookmark 203 | :param torrent_id: 204 | :return: number of torrents added 205 | """ 206 | return self._query('/bookmarks/save/%d' % torrent_id) 207 | 208 | def del_bookmark(self, *args): 209 | """ 210 | Remove a bookmark 211 | :param args: tuple of torrent id's 212 | :return: Number of torrents deleted 213 | """ 214 | query = ','.join([str(i) for i in args]) 215 | return self._query('/bookmarks/delete/%s' % query) 216 | -------------------------------------------------------------------------------- /t411api/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | from .API import T411API, APIError, \ 4 | ServiceError, ConnectError 5 | 6 | __package__ = 't411api' 7 | -------------------------------------------------------------------------------- /t411api/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple functions widely used in the program 3 | """ 4 | 5 | 6 | def sanitize(value): 7 | """ 8 | Normalizes string, converts to lowercase, removes non-alpha characters, 9 | and converts spaces to underscores. 10 | :param value: unsafe string 11 | :return safe string 12 | """ 13 | from re import sub 14 | from unicodedata import normalize 15 | value = normalize('NFKD', value).encode('ascii', 'ignore') 16 | value = sub('[^\w\s\.-]', '', value.decode('utf-8')).strip().lower() 17 | return sub('[-_\s]+', '_', value) 18 | -------------------------------------------------------------------------------- /t411api/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.7' 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xide/t411api/8e9a787034051873071c94fc708d30816d65c46d/tests/__init__.py --------------------------------------------------------------------------------