├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── avatar.png ├── bbb.torrent └── examples.py ├── pyproject.toml ├── setup.py └── src └── rdapi ├── __init__.py ├── error_codes.json └── rdapi.py /.env.example: -------------------------------------------------------------------------------- 1 | RD_APITOKEN="your_token_here" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pypirc 3 | .env 4 | *.egg-info 5 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [$krilla] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rd_api_py 2 | 3 | Real Debrid API Library for Python 4 | 5 | ## Installation 6 | 7 | Install the package 8 | 9 | ```bash 10 | python -m pip install rd_api_py 11 | ``` 12 | 13 | Set `.env` environment variables in `rd_refresh` or your script 14 | 15 | ```bash 16 | RD_APITOKEN="your_token_here" 17 | 18 | # Optional, defaults recommended 19 | SLEEP=2000 # Delay (ms) between requests 20 | LONG_SLEEP=30000 # Long delay (ms) every 500 requests 21 | ``` 22 | 23 | ## Usage 24 | 25 | All operations are supported - see examples directory 26 | 27 | ```python 28 | from rdapi import RD 29 | 30 | RD = RD() 31 | 32 | print(RD.system.time().content) 33 | print(RD.user.get().json()) 34 | print(RD.torrents.get(limit=10, page=1).json()) 35 | 36 | import os 37 | filepath = '/path/to/bbb.torrent' 38 | print(RD.torrents.add_file(filepath=filepath).json()) 39 | 40 | # etc... 41 | ``` -------------------------------------------------------------------------------- /examples/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-krilla/rd_api_py/1bcf7b2d9c0900638471812cd8efb5235ee7fc3e/examples/avatar.png -------------------------------------------------------------------------------- /examples/bbb.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-krilla/rd_api_py/1bcf7b2d9c0900638471812cd8efb5235ee7fc3e/examples/bbb.torrent -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from rdapi import RD 4 | 5 | RD = RD() 6 | 7 | ''' 8 | System 9 | 10 | ''' 11 | 12 | RD.system.disable_token() 13 | 14 | RD.system.time().content 15 | 16 | RD.system.iso_time().content 17 | 18 | ''' 19 | User 20 | 21 | ''' 22 | 23 | RD.user.get().json() 24 | 25 | ''' 26 | Unrestrict 27 | 28 | ''' 29 | 30 | RD.unrestrict.check(link="https://www.youtube.com/watch?v=aqz-KE-bpKQ").json() 31 | 32 | RD.unrestrict.link(link="https://real-debrid.com/d/example").json() 33 | 34 | RD.unrestrict.folder(link="https://clicknupload.vip/example").json() 35 | 36 | import os 37 | filepath = os.getcwd() + '/Testpackage.dlc' 38 | RD.unrestrict.container_file(filepath).json() 39 | 40 | RD.unrestrict.container_link(link="example").json() 41 | 42 | ''' 43 | Traffic 44 | 45 | ''' 46 | 47 | RD.traffic.get().json() 48 | 49 | import datetime 50 | start = datetime.datetime(2023, 6, 25) 51 | end = datetime.datetime(2023, 9, 10) 52 | RD.traffic.details(start, end).json() 53 | 54 | ''' 55 | Streaming 56 | 57 | ''' 58 | 59 | RD.streaming.transcode('example_id').json() 60 | 61 | RD.streaming.media_info('example_id').json() 62 | 63 | ''' 64 | Downloads 65 | 66 | ''' 67 | 68 | RD.downloads.get(limit=10, page=2).json() 69 | 70 | RD.downloads.delete('example_id') 71 | 72 | ''' 73 | Torrents 74 | 75 | ''' 76 | 77 | RD.torrents.get(limit=10, page=1).json() 78 | 79 | RD.torrents.info('example_id').json() 80 | 81 | RD.torrents.instant_availability('FE7E3784A298169D8DE3804B8FDE5EC318105194').json() 82 | 83 | RD.torrents.active_count().json() 84 | 85 | RD.torrents.available_hosts().json() 86 | 87 | import os 88 | filepath = os.getcwd() + '/bbb.torrent' 89 | 90 | RD.torrents.add_file(filepath=filepath).json() 91 | 92 | RD.torrents.add_magnet('FE7E3784A298169D8DE3804B8FDE5EC318105194').json() 93 | 94 | RD.torrents.select_files('example_id', 'all') 95 | 96 | RD.torrents.delete('example_id') 97 | 98 | ''' 99 | Hosts 100 | 101 | ''' 102 | 103 | RD.hosts.get().json() 104 | 105 | RD.hosts.status().json() 106 | 107 | RD.hosts.regex().json() 108 | 109 | RD.hosts.regex_folder().json() 110 | 111 | RD.hosts.domains().json() 112 | 113 | ''' 114 | Settings 115 | 116 | ''' 117 | 118 | RD.settings.get().json() 119 | 120 | RD.settings.update('streaming_language_preference', 'eng') 121 | 122 | RD.settings.convert_points() 123 | 124 | RD.settings.change_password() 125 | 126 | import os 127 | filepath = os.getcwd() + '/avatar.png' 128 | 129 | RD.settings.avatar_file(filepath) 130 | 131 | RD.settings.avatar_delete() 132 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rd_api_py" 3 | version = "0.4.0" 4 | authors = [ 5 | { name="$krilla" }, 6 | ] 7 | description = "Real Debrid API Library for Python" 8 | readme = "README.md" 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Programming Language :: Python :: 3", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | ] 15 | dependencies = [ 16 | "requests ~=2.31.0", 17 | "python-dotenv ~=1.0.0", 18 | ] 19 | 20 | [tool.setuptools.packages.find] 21 | where = ["src"] 22 | 23 | [tool.setuptools.package-data] 24 | rdapi = ["*.json"] 25 | 26 | [project.urls] 27 | "Homepage" = "https://github.com/s-krilla/rd_api_py" 28 | "Bug Tracker" = "https://github.com/s-krilla/rd_api_py/issues" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() -------------------------------------------------------------------------------- /src/rdapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .rdapi import RD -------------------------------------------------------------------------------- /src/rdapi/error_codes.json: -------------------------------------------------------------------------------- 1 | { 2 | "-1": "Internal error", 3 | "1": "Missing parameter", 4 | "2": "Bad parameter value", 5 | "3": "Unknown method", 6 | "4": "Method not allowed", 7 | "5": "Slow down", 8 | "6": "Ressource unreachable", 9 | "7": "Resource not found", 10 | "8": "Bad token", 11 | "9": "Permission denied", 12 | "10": "Two-Factor authentication needed", 13 | "11": "Two-Factor authentication pending", 14 | "12": "Invalid login", 15 | "13": "Invalid password", 16 | "14": "Account locked", 17 | "15": "Account not activated", 18 | "16": "Unsupported hoster", 19 | "17": "Hoster in maintenance", 20 | "18": "Hoster limit reached", 21 | "19": "Hoster temporarily unavailable", 22 | "20": "Hoster not available for free users", 23 | "21": "Too many active downloads", 24 | "22": "IP Address not allowed", 25 | "23": "Traffic exhausted", 26 | "24": "File unavailable", 27 | "25": "Service unavailable", 28 | "26": "Upload too big", 29 | "27": "Upload error", 30 | "28": "File not allowed", 31 | "29": "Torrent too big", 32 | "30": "Torrent file invalid", 33 | "31": "Action already done", 34 | "32": "Image resolution error", 35 | "33": "Torrent already active", 36 | "34": "Too many requests", 37 | "35": "Infringing file", 38 | "36": "Fair Usage Limit" 39 | } -------------------------------------------------------------------------------- /src/rdapi/rdapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import time 5 | import json 6 | import logging 7 | import requests 8 | import itertools 9 | from pathlib import Path 10 | 11 | class RD: 12 | def __init__(self): 13 | self.rd_apitoken = os.getenv('RD_APITOKEN') 14 | self.base_url = 'https://api.real-debrid.com/rest/1.0' 15 | self.header = {'Authorization': "Bearer " + str(self.rd_apitoken)} 16 | self.error_codes = json.load(open(os.path.join(Path(__file__).parent.absolute(), 'error_codes.json'))) 17 | self.sleep = int(os.getenv('SLEEP', 2000)) / 1000 18 | self.long_sleep = int(os.getenv('LONG_SLEEP', 30000)) / 1000 19 | self.count_obj = itertools.cycle(range(0, 501)) 20 | self.count = next(self.count_obj) 21 | 22 | # Check the API token 23 | self.check_token() 24 | 25 | self.system = self.System(self) 26 | self.user = self.User(self) 27 | self.unrestrict = self.Unrestrict(self) 28 | self.traffic = self.Traffic(self) 29 | self.streaming = self.Streaming(self) 30 | self.downloads = self.Downloads(self) 31 | self.torrents = self.Torrents(self) 32 | self.hosts = self.Hosts(self) 33 | self.settings = self.Settings(self) 34 | 35 | def get(self, path, **options): 36 | request = requests.get(self.base_url + path, headers=self.header, params=options) 37 | return self.handler(request, self.error_codes, path) 38 | 39 | def post(self, path, **payload): 40 | request = requests.post(self.base_url + path, headers=self.header, data=payload) 41 | return self.handler(request, self.error_codes, path) 42 | 43 | def put(self, path, filepath, **payload): 44 | with open(filepath, 'rb') as file: 45 | request = requests.put(self.base_url + path, headers=self.header, data=file, params=payload) 46 | return self.handler(request, self.error_codes, path) 47 | 48 | def delete(self, path): 49 | request = requests.delete(self.base_url + path, headers=self.header) 50 | return self.handler(request, self.error_codes, path) 51 | 52 | def handler(self, request, error_codes, path): 53 | try: 54 | request.raise_for_status() 55 | except requests.exceptions.HTTPError as errh: 56 | logging.error('%s at %s', errh, path) 57 | except requests.exceptions.ConnectionError as errc: 58 | logging.error('%s at %s', errc, path) 59 | except requests.exceptions.Timeout as errt: 60 | logging.error('%s at %s', errt, path) 61 | except requests.exceptions.RequestException as err: 62 | logging.error('%s at %s', err, path) 63 | try: 64 | if 'error_code' in request.json(): 65 | code = request.json()['error_code'] 66 | message = error_codes.get(str(code), 'Unknown error') 67 | logging.warning('%s: %s at %s', code, message, path) 68 | except: 69 | pass 70 | self.handle_sleep() 71 | return request 72 | 73 | def check_token(self): 74 | if self.rd_apitoken is None or self.rd_apitoken == 'your_token_here': 75 | logging.warning('Add token to .env') 76 | 77 | def handle_sleep(self): 78 | if self.count < 500: 79 | logging.debug('Sleeping %ss', self.sleep) 80 | time.sleep(self.sleep) 81 | elif self.count == 500: 82 | logging.debug('Sleeping %ss', self.long_sleep) 83 | time.sleep(self.long_sleep) 84 | self.count = 0 85 | 86 | class System: 87 | def __init__(self, rd_instance): 88 | self.rd = rd_instance 89 | 90 | def disable_token(self): 91 | return self.rd.get('/disable_access_token') 92 | 93 | def time(self): 94 | return self.rd.get('/time') 95 | 96 | def iso_time(self): 97 | return self.rd.get('/time/iso') 98 | 99 | class User: 100 | def __init__(self, rd_instance): 101 | self.rd = rd_instance 102 | 103 | def get(self): 104 | return self.rd.get('/user') 105 | 106 | class Unrestrict: 107 | def __init__(self, rd_instance): 108 | self.rd = rd_instance 109 | 110 | def check(self, link, password=None): 111 | return self.rd.post('/unrestrict/check', link=link, password=password) 112 | 113 | def link(self, link, password=None, remote=None): 114 | return self.rd.post('/unrestrict/link', link=link, password=password, remote=remote) 115 | 116 | def folder(self, link): 117 | return self.rd.post('/unrestrict/folder', link=link) 118 | 119 | def container_file(self, filepath): 120 | return self.rd.put('/unrestrict/containerFile', filepath=filepath) 121 | 122 | def container_link(self, link): 123 | return self.rd.post('/unrestrict/containerLink', link=link) 124 | 125 | class Traffic: 126 | def __init__(self, rd_instance): 127 | self.rd = rd_instance 128 | 129 | def get(self): 130 | return self.rd.get('/traffic') 131 | 132 | def details(self, start=None, end=None): 133 | return self.rd.get('/traffic/details', start=start, end=end) 134 | 135 | class Streaming: 136 | def __init__(self, rd_instance): 137 | self.rd = rd_instance 138 | 139 | def transcode(self, id): 140 | return self.rd.get('/streaming/transcode/' + str(id)) 141 | 142 | def media_info(self, id): 143 | return self.rd.get('/streaming/mediaInfos/' + str(id)) 144 | 145 | class Downloads: 146 | def __init__(self, rd_instance): 147 | self.rd = rd_instance 148 | 149 | def get(self, offset=None, page=None, limit=None ): 150 | return self.rd.get('/downloads', offset=offset, page=page, limit=limit) 151 | 152 | def delete(self, id): 153 | return self.rd.delete('/downloads/delete/'+ str(id)) 154 | 155 | class Torrents: 156 | def __init__(self, rd_instance): 157 | self.rd = rd_instance 158 | 159 | def get(self, offset=None, page=None, limit=None, filter=None ): 160 | return self.rd.get('/torrents', offset=offset, page=page, limit=limit, filter=filter) 161 | 162 | def info(self, id): 163 | return self.rd.get('/torrents/info/' + str(id)) 164 | 165 | def instant_availability(self, hash): 166 | return self.rd.get('/torrents/instantAvailability/' + str(hash)) 167 | 168 | def active_count(self): 169 | return self.rd.get('/torrents/activeCount') 170 | 171 | def available_hosts(self): 172 | return self.rd.get('/torrents/availableHosts') 173 | 174 | def add_file(self, filepath, host=None): 175 | return self.rd.put('/torrents/addTorrent', filepath=filepath, host=host) 176 | 177 | def add_magnet(self, magnet, host=None): 178 | magnet_link = 'magnet:?xt=urn:btih:' + str(magnet) 179 | return self.rd.post('/torrents/addMagnet', magnet=magnet_link, host=host) 180 | 181 | def select_files(self, id, files): 182 | return self.rd.post('/torrents/selectFiles/' + str(id), files=str(files)) 183 | 184 | def delete(self, id): 185 | return self.rd.delete('/torrents/delete/' + str(id)) 186 | 187 | class Hosts: 188 | def __init__(self, rd_instance): 189 | self.rd = rd_instance 190 | 191 | def get(self): 192 | return self.rd.get('/hosts') 193 | 194 | def status(self): 195 | return self.rd.get('/hosts/status') 196 | 197 | def regex(self): 198 | return self.rd.get('/hosts/regex') 199 | 200 | def regex_folder(self): 201 | return self.rd.get('/hosts/regexFolder') 202 | 203 | def domains(self): 204 | return self.rd.get('/hosts/domains') 205 | 206 | class Settings: 207 | def __init__(self, rd_instance): 208 | self.rd = rd_instance 209 | 210 | def get(self): 211 | return self.rd.get('/settings') 212 | 213 | def update(self, setting_name, setting_value): 214 | return self.rd.post('/settings/update', setting_name=setting_name, setting_value=setting_value) 215 | 216 | def convert_points(self): 217 | return self.rd.post('/settings/convertPoints') 218 | 219 | def change_password(self): 220 | return self.rd.post('/settings/changePassword') 221 | 222 | def avatar_file(self, filepath): 223 | return self.rd.put('/settings/avatarFile', filepath=filepath) 224 | 225 | def avatar_delete(self): 226 | return self.rd.delete('/settings/avatarDelete') 227 | --------------------------------------------------------------------------------