├── .gitignore ├── .python-version ├── README.md ├── pyproject.toml ├── torbox-magnet-importer.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # torbox-dmm-sync 2 | 3 | > Sync DMM magnet links to TorBox. 4 | 5 | Credits: https://gist.github.com/eliasbenb/10a4a49f3feb9df19b0b8ed838babb82 6 | 7 | ## Usage instructions 8 | 9 | 0. Export and download your DMM backup JSON from: https://debridmediamanager.com/library 10 | 1. Install `uv`. See: https://docs.astral.sh/uv/getting-started/installation/ 11 | - Also create and activate the virtual environment: `uv venv` and `source .venv/bin/activate` 12 | 13 | 2. Run the script using one of these methods: 14 | ```bash 15 | # Using environment variables 16 | TORBOX_API_KEY="your-api-key" DMM_BACKUP_JSON_FILE="path/to/dmm-backup.json" uv run torbox-magnet-importer.py 17 | 18 | # Using command line arguments 19 | uv run torbox-magnet-importer.py --api-key "your-api-key" --input-file "path/to/dmm-backup.json" 20 | 21 | # Dry run (simulate without making changes) 22 | uv run torbox-magnet-importer.py --api-key "your-api-key" --input-file "path/to/dmm-backup.json" --dry-run 23 | ``` 24 | 25 | ### Options 26 | 27 | - `--api-key`: TorBox API key (can also use TORBOX_API_KEY env var) 28 | - `--input-file`: Path to DMM backup JSON file (can also use DMM_BACKUP_JSON_FILE env var) 29 | - `--dry-run`: Perform a dry run without making any changes 30 | - `--no-log-file`: Disable logging to file 31 | 32 | Get torbox API key from: https://torbox.app/settings -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "torbox-dmm-sync" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "requests>=2.32.3", 9 | ] 10 | -------------------------------------------------------------------------------- /torbox-magnet-importer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S uv run 2 | # /// script 3 | # requires-python = ">=3.12" 4 | # dependencies = [ 5 | # "requests", 6 | # ] 7 | # /// 8 | 9 | import requests 10 | import logging 11 | from typing import List, Set 12 | import time 13 | from datetime import datetime 14 | import os 15 | import json 16 | import argparse 17 | from dataclasses import dataclass 18 | 19 | @dataclass 20 | class Magnet: 21 | hash: str 22 | filename: str | None = None 23 | 24 | def to_uri(self) -> str: 25 | """Generate a magnet URI from the hash and filename.""" 26 | uri = f"magnet:?xt=urn:btih:{self.hash}" 27 | if self.filename: 28 | uri += f"&dn={self.filename}" 29 | return uri 30 | 31 | def setup_logging(log_to_file: bool = True) -> None: 32 | """ 33 | Configure the logging system with both console and file output. 34 | 35 | Args: 36 | log_to_file (bool): If True, logs will be written to a timestamped file in addition to console output. 37 | If False, logs will only be written to console. 38 | 39 | The log file name format is: torbox_sync_YYYYMMDD_HHMMSS.log 40 | """ 41 | handlers = [logging.StreamHandler()] 42 | if log_to_file: 43 | handlers.append( 44 | logging.FileHandler( 45 | f'torbox_sync_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log' 46 | ) 47 | ) 48 | 49 | logging.basicConfig( 50 | level=logging.INFO, 51 | format="%(asctime)s - %(levelname)s - %(message)s", 52 | handlers=handlers, 53 | ) 54 | 55 | class TorBoxManager: 56 | """ 57 | Manages interactions with the TorBox API for torrent operations. 58 | 59 | This class handles retrieving existing torrents, loading magnet links from DMM backup files, 60 | and creating new torrents in TorBox. 61 | 62 | Attributes: 63 | api_key (str): The TorBox API authentication key 64 | base_url (str): The base URL for TorBox API endpoints 65 | headers (dict): HTTP headers including authentication and cache control 66 | dry_run (bool): If True, simulates operations without making actual API calls 67 | """ 68 | 69 | def __init__(self, api_key: str, base_url: str = "https://api.torbox.app/v1", dry_run: bool = False): 70 | """ 71 | Initialize the TorBox manager. 72 | 73 | Args: 74 | api_key (str): TorBox API key for authentication 75 | base_url (str): Base URL for the TorBox API 76 | dry_run (bool): If True, simulates operations without making actual changes 77 | """ 78 | self.api_key = api_key 79 | self.base_url = base_url 80 | self.headers = {"Authorization": f"Bearer {api_key}", "bypass_cache": "true", "limit": "2147483647"} 81 | self.dry_run = dry_run 82 | 83 | def _make_request(self, method: str, endpoint: str, max_retries: int = 3, **kwargs) -> requests.Response: 84 | """ 85 | Make an HTTP request to the TorBox API with retry logic and exponential backoff. 86 | 87 | This method wraps the requests library to provide: 88 | - Automatic retry on failure with exponential backoff 89 | - Consistent header injection 90 | - URL construction from base URL and endpoint 91 | - Error logging and handling 92 | 93 | Args: 94 | method (str): HTTP method to use (e.g., 'GET', 'POST') 95 | endpoint (str): API endpoint to call (e.g., 'api/torrents/mylist') 96 | max_retries (int, optional): Maximum number of retry attempts. Defaults to 3. 97 | **kwargs: Additional arguments to pass to requests.request() 98 | Common kwargs include: 99 | - params: dict of URL parameters 100 | - data: dict of form data 101 | - json: dict to send as JSON 102 | 103 | Returns: 104 | requests.Response: The successful response from the API 105 | 106 | Raises: 107 | requests.exceptions.RequestException: If all retry attempts fail 108 | 109 | Example: 110 | response = self._make_request('GET', 'api/torrents/mylist', params={'limit': 100}) 111 | """ 112 | # Add authorization and cache control headers to the request 113 | kwargs["headers"] = self.headers 114 | 115 | # Construct the full URL by combining base URL and endpoint 116 | url = f"{self.base_url}/{endpoint}" 117 | 118 | # Try the request up to max_retries times 119 | for attempt in range(max_retries): 120 | try: 121 | # Log the attempt and make the request 122 | logging.info(f"Requesting {url} with method {method} (attempt {attempt + 1}/{max_retries})") 123 | res = requests.request(method, url, **kwargs) 124 | 125 | # Raise an exception for bad status codes (4xx, 5xx) 126 | res.raise_for_status() 127 | 128 | # Add a small delay after successful request to prevent rate limiting 129 | time.sleep(5 * (attempt + 1)) 130 | 131 | return res 132 | 133 | except requests.exceptions.RequestException as e: 134 | # Calculate wait time using exponential backoff: 5, 10, 20 seconds... 135 | wait_time = 5 * (2**attempt) 136 | logging.warning(f"Attempt {attempt + 1}/{max_retries} failed: {str(e)}") 137 | 138 | if attempt < max_retries - 1: 139 | # If we have more retries left, wait and try again 140 | logging.info(f"Retrying in {wait_time} seconds...") 141 | time.sleep(wait_time) 142 | else: 143 | # If we're out of retries, log the error and re-raise the exception 144 | logging.error(f"Max retries reached for {endpoint}") 145 | raise 146 | 147 | def get_existing_torrents(self) -> Set[str]: 148 | """ 149 | Retrieve all existing and queued torrents from TorBox. 150 | 151 | Returns: 152 | Set[str]: A set of lowercase torrent hashes that already exist in TorBox 153 | 154 | The method makes two API calls: 155 | 1. Gets the list of existing torrents 156 | 2. Gets the list of queued torrents 157 | 158 | A delay of 5 seconds is added between API calls to prevent rate limiting. 159 | """ 160 | if self.dry_run: 161 | logging.info("[DRY RUN] Would fetch existing torrents") 162 | return set() 163 | 164 | existing_hashes = set() 165 | 166 | # First, get existing torrents 167 | try: 168 | response = self._make_request( 169 | "GET", "api/torrents/mylist", params={"bypass_cache": "true"} 170 | ) 171 | response.raise_for_status() 172 | data = response.json() 173 | 174 | if data.get("success"): 175 | for torrent in data.get("data", []): 176 | if torrent.get("hash"): 177 | existing_hashes.add(torrent["hash"].lower()) 178 | else: 179 | logging.error(f"Failed to get existing torrents: {data.get('detail')}") 180 | except Exception as e: 181 | logging.error(f"Error getting existing torrents: {str(e)}") 182 | 183 | # Wait before making the second API call 184 | time.sleep(5) 185 | 186 | # Then, get queued torrents 187 | try: 188 | response = self._make_request("GET", "api/torrents/getqueued") 189 | response.raise_for_status() 190 | data = response.json() 191 | 192 | if data.get("success"): 193 | for torrent in data.get("data", []): 194 | if torrent.get("hash"): 195 | existing_hashes.add(torrent["hash"].lower()) 196 | else: 197 | logging.error(f"Failed to get queued torrents: {data.get('detail')}") 198 | except Exception as e: 199 | logging.error(f"Error getting queued torrents: {str(e)}") 200 | 201 | logging.info(f"Found {len(existing_hashes)} existing/queued torrents") 202 | return existing_hashes 203 | 204 | def load_magnet_links(self, filename: str) -> List[Magnet]: 205 | """ 206 | Load and convert torrent information from a DMM backup JSON file into Magnet objects. 207 | 208 | Args: 209 | filename (str): Path to the DMM backup JSON file 210 | 211 | Returns: 212 | List[Magnet]: List of Magnet objects generated from the backup file 213 | """ 214 | try: 215 | with open(filename, "r", encoding="utf-8") as f: 216 | data = json.load(f) 217 | 218 | magnets = [] 219 | for item in data: 220 | if "hash" in item: 221 | magnet = Magnet( 222 | hash=item["hash"], 223 | filename=item.get("filename") # Use get() to handle missing filename 224 | ) 225 | magnets.append(magnet) 226 | 227 | logging.info(f"Loaded {len(magnets)} magnet links from {filename}") 228 | return magnets 229 | except Exception as e: 230 | logging.error(f"Error loading magnet links from JSON file: {str(e)}") 231 | return [] 232 | 233 | def create_torrent(self, magnet: Magnet) -> bool: 234 | """ 235 | Create a new torrent in TorBox using a magnet link. 236 | 237 | Args: 238 | magnet (Magnet): The Magnet object to add 239 | 240 | Returns: 241 | bool: True if the torrent was added successfully, False otherwise 242 | """ 243 | if self.dry_run: 244 | logging.info(f"[DRY RUN] Would add torrent: {magnet.hash}") 245 | return True 246 | 247 | try: 248 | response = self._make_request( 249 | "POST", "api/torrents/createtorrent", data={"magnet": magnet.to_uri()} 250 | ) 251 | response.raise_for_status() 252 | data = response.json() 253 | 254 | if data.get("success"): 255 | logging.info(f"Successfully added torrent: {data.get('detail')}") 256 | return True 257 | else: 258 | logging.error(f"Failed to add torrent: {data.get('detail')}") 259 | return False 260 | except Exception as e: 261 | logging.error(f"Error creating torrent: {str(e)}") 262 | return False 263 | 264 | def process_magnets(self, magnets: List[Magnet], existing_hashes: Set[str]): 265 | """ 266 | Process a list of Magnet objects and add them to TorBox if they don't already exist. 267 | 268 | Args: 269 | magnets (List[Magnet]): List of Magnet objects to process 270 | existing_hashes (Set[str]): Set of torrent hashes that already exist in TorBox 271 | 272 | Returns: 273 | int: Number of successfully added torrents 274 | """ 275 | total = len(magnets) 276 | successful = 0 277 | 278 | for idx, magnet in enumerate(magnets, 1): 279 | torrent_hash = magnet.hash.lower() 280 | 281 | if torrent_hash in existing_hashes: 282 | logging.info( 283 | f"Skipping existing torrent ({idx}/{total}): {torrent_hash}" 284 | ) 285 | continue 286 | 287 | if self.create_torrent(magnet): 288 | successful += 1 289 | 290 | logging.info(f"Progress: {idx}/{total} processed ({successful} added)") 291 | 292 | # Add delay between API calls if not in dry-run mode 293 | if not self.dry_run: 294 | time.sleep(5) 295 | 296 | logging.info( 297 | f"Completed processing {total} magnets. Successfully added {successful} new torrents." 298 | ) 299 | return successful 300 | 301 | def parse_args(): 302 | """ 303 | Parse command line arguments for the script. 304 | 305 | Returns: 306 | argparse.Namespace: Parsed command line arguments 307 | 308 | Supported arguments: 309 | - --api-key: TorBox API key (can also use TORBOX_API_KEY env var) 310 | - --input-file: Path to DMM backup JSON file (can also use DMM_BACKUP_JSON_FILE env var) 311 | - --dry-run: Perform a dry run without making any changes 312 | - --no-log-file: Disable logging to file 313 | """ 314 | parser = argparse.ArgumentParser(description="Sync DMM magnet links to TorBox") 315 | parser.add_argument( 316 | "--api-key", 317 | default=os.getenv("TORBOX_API_KEY"), 318 | help="TorBox API key (default: TORBOX_API_KEY env var)", 319 | ) 320 | parser.add_argument( 321 | "--input-file", 322 | default=os.getenv("DMM_BACKUP_JSON_FILE", "dmm-backup.json"), 323 | help="DMM backup JSON file (default: DMM_BACKUP_JSON_FILE env var or dmm-backup.json)", 324 | ) 325 | parser.add_argument( 326 | "--dry-run", 327 | action="store_true", 328 | help="Perform a dry run without making any changes", 329 | ) 330 | parser.add_argument( 331 | "--no-log-file", 332 | action="store_true", 333 | help="Disable logging to file", 334 | ) 335 | return parser.parse_args() 336 | 337 | def main(): 338 | """ 339 | Main entry point for the script. 340 | 341 | The function: 342 | 1. Parses command line arguments 343 | 2. Sets up logging 344 | 3. Creates a TorBoxManager instance 345 | 4. Gets existing torrents from TorBox 346 | 5. Loads magnet links from the DMM backup file 347 | 6. Processes the magnet links and adds new torrents to TorBox 348 | 349 | Returns: 350 | int: 0 for success, 1 for failure 351 | """ 352 | args = parse_args() 353 | 354 | setup_logging(not args.no_log_file) 355 | 356 | if not args.api_key: 357 | logging.error("No API key provided. Use --api-key or set TORBOX_API_KEY environment variable") 358 | return 1 359 | 360 | if args.dry_run: 361 | logging.info("Running in dry-run mode - no changes will be made") 362 | 363 | torbox = TorBoxManager(args.api_key, dry_run=args.dry_run) 364 | 365 | existing_hashes = torbox.get_existing_torrents() 366 | 367 | magnets = torbox.load_magnet_links(args.input_file) 368 | if not magnets: 369 | logging.error("No magnet links loaded. Exiting.") 370 | return 1 371 | 372 | torbox.process_magnets(magnets, existing_hashes) 373 | return 0 374 | 375 | if __name__ == "__main__": 376 | exit(main()) 377 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "certifi" 6 | version = "2024.8.30" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, 11 | ] 12 | 13 | [[package]] 14 | name = "charset-normalizer" 15 | version = "3.4.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } 18 | wheels = [ 19 | { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, 20 | { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, 21 | { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, 22 | { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, 23 | { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, 24 | { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, 25 | { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, 26 | { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, 27 | { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, 28 | { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, 29 | { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, 30 | { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, 31 | { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, 32 | { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, 33 | { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, 34 | { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, 35 | { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, 36 | { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, 37 | { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, 38 | { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, 39 | { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, 40 | { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, 41 | { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, 42 | { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, 43 | { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, 44 | { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, 45 | { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, 46 | { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, 47 | { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, 48 | { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, 49 | { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, 50 | ] 51 | 52 | [[package]] 53 | name = "idna" 54 | version = "3.10" 55 | source = { registry = "https://pypi.org/simple" } 56 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 57 | wheels = [ 58 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 59 | ] 60 | 61 | [[package]] 62 | name = "requests" 63 | version = "2.32.3" 64 | source = { registry = "https://pypi.org/simple" } 65 | dependencies = [ 66 | { name = "certifi" }, 67 | { name = "charset-normalizer" }, 68 | { name = "idna" }, 69 | { name = "urllib3" }, 70 | ] 71 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 72 | wheels = [ 73 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 74 | ] 75 | 76 | [[package]] 77 | name = "torbox-dmm-sync" 78 | version = "0.1.0" 79 | source = { virtual = "." } 80 | dependencies = [ 81 | { name = "requests" }, 82 | ] 83 | 84 | [package.metadata] 85 | requires-dist = [{ name = "requests", specifier = ">=2.32.3" }] 86 | 87 | [[package]] 88 | name = "urllib3" 89 | version = "2.2.3" 90 | source = { registry = "https://pypi.org/simple" } 91 | sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, 94 | ] 95 | --------------------------------------------------------------------------------