seedr-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
seedr_py-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Developer
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.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: seedr-py
3
+ Version: 0.1.0
4
+ Summary: Modern Python client and API wrapper for Seedr.cc with zero-CAPTCHA authentication, folder ZIP streaming, and HLS playback.
5
+ Author-email: Nishant <nishantprime@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nishantprime/seedr-py
8
+ Project-URL: Bug Tracker, https://github.com/nishantprime/seedr-py/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.28.0
22
+ Dynamic: license-file
23
+
24
+ # seedr-py ⚡
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/seedr-py.svg)](https://pypi.org/project/seedr-py/)
27
+ [![Python versions](https://img.shields.io/pypi/pyversions/seedr-py.svg)](https://pypi.org/project/seedr-py/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
29
+
30
+ A modern, high-speed Python client and API wrapper for **[Seedr.cc](https://www.seedr.cc)**.
31
+
32
+ Designed to overcome the limitations of legacy wrappers: provides **zero-CAPTCHA email/password authentication**, **one-click full folder ZIP downloads without mCaptcha**, **browser-compatible HLS video streaming**, and clean dataclass models.
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+
38
+ - 🚀 **Zero-CAPTCHA Login:** Authenticates programmatically via Seedr's internal identity handshake (`seedr_sbvt`). No browser automation or CAPTCHA solving required.
39
+ - 📦 **One-Click Folder ZIP Downloads:** Download entire subfolder structures as a single ZIP archive directly from Seedr CDN with zero mCaptcha prompts.
40
+ - 🎬 **HLS Video Streaming:** Access both raw 1080p/4K bitrate files and browser-compatible HLS (`.m3u8`) streams with auto-converted AAC stereo audio (fixing silent Dolby EAC3 audio in Chrome/Firefox).
41
+ - 🧲 **Magnets & `.torrent` Files:** Supports magnet links, direct torrent URLs, and local `.torrent` file uploads.
42
+ - 📂 **Full Cloud File Management:** List directories, inspect quotas, retrieve direct CDN download links, cancel downloading tasks, and batch delete.
43
+ - 🛡️ **Fully Typed Dataclasses:** Every response returns rich, typed models (`FolderContents`, `SeedrFile`, `Quota`, `AccountSettings`, `VideoStream`).
44
+
45
+ ---
46
+
47
+ ## 📦 Installation
48
+
49
+ ```bash
50
+ pip install seedr-py
51
+ ```
52
+
53
+ ---
54
+
55
+ ## ⚡ Quickstart
56
+
57
+ ### 1. Authenticate
58
+
59
+ ```python
60
+ from seedr_py import SeedrClient
61
+
62
+ # Login with email and password (Zero CAPTCHA)
63
+ client = SeedrClient.from_password("your_email@example.com", "your_password")
64
+
65
+ # Or authenticate using cookies
66
+ client = SeedrClient.from_cookies({
67
+ "seedr_ptok": "your_token",
68
+ "RSESS_remember": "your_session"
69
+ })
70
+ ```
71
+
72
+ ### 2. Check Quota & Account
73
+
74
+ ```python
75
+ quota = client.get_quota()
76
+ print(f"Storage: {quota.space_used_formatted} / {quota.space_max_formatted} ({quota.space_pct}%)")
77
+
78
+ account = client.get_account_settings()
79
+ print(f"Logged in as: {account.email} (User ID: {account.user_id})")
80
+ ```
81
+
82
+ ### 3. Browse Files & Folders
83
+
84
+ ```python
85
+ # folder_id=0 represents the root directory
86
+ root = client.get_folder(0)
87
+
88
+ # List subfolders
89
+ for folder in root.folders:
90
+ print(f"📁 {folder.name} (ID: {folder.id}, Size: {folder.size_formatted})")
91
+
92
+ # List files
93
+ for file in root.files:
94
+ print(f"📄 {file.name} (ID: {file.id}, Size: {file.size_formatted})")
95
+
96
+ # List active torrent transfers
97
+ for task in root.tasks:
98
+ print(f"⚡ {task.name}: {task.progress}% @ {task.download_rate / 1024:.1f} KB/s")
99
+ ```
100
+
101
+ ### 4. Download Entire Folder as a ZIP (No CAPTCHA)
102
+
103
+ ```python
104
+ # 1. Get the direct signed ZIP URL from Seedr CDN
105
+ archive = client.get_folder_zip_url(folder_id=1438069680)
106
+ print("Signed ZIP Download Link:", archive.url)
107
+
108
+ # 2. Or stream it directly to disk
109
+ client.download_folder_zip(folder_id=1438069680, save_path="MyFolder.zip")
110
+ ```
111
+
112
+ ### 5. Add Torrents & Magnet Links
113
+
114
+ ```python
115
+ # Add a magnet link
116
+ task = client.add_magnet("magnet:?xt=urn:btih:08ada5a7a6183aae1e0908bc1376f4c61b03387c...")
117
+ print(f"Transfer queued! Torrent ID: {task.get('user_torrent_id')}")
118
+
119
+ # Or upload a local .torrent file
120
+ client.add_torrent_file("ubuntu-24.04.torrent")
121
+ ```
122
+
123
+ ### 6. Stream Videos with Browser-Compatible Audio
124
+
125
+ ```python
126
+ stream = client.get_video_stream(file_id=6018680430)
127
+
128
+ # Universal HLS stream with converted AAC audio
129
+ print("HLS Stream URL:", stream.hls_url)
130
+
131
+ # Raw untouched 1080p/4K bitrate stream (for VLC / external players)
132
+ print("Raw Stream URL:", stream.raw_url)
133
+
134
+ # Multi-language subtitle tracks (.vtt)
135
+ print("Subtitles:", stream.subtitles)
136
+ ```
137
+
138
+ ### 7. Delete Files, Folders, or Tasks
139
+
140
+ ```python
141
+ # Cancel an active downloading torrent
142
+ client.cancel_task(task_id=200392405)
143
+
144
+ # Delete a folder
145
+ client.delete_folder(folder_id=1438069680)
146
+
147
+ # Delete a file
148
+ client.delete_file(file_id=6018680430)
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 📜 License
154
+
155
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,132 @@
1
+ # seedr-py ⚡
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/seedr-py.svg)](https://pypi.org/project/seedr-py/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/seedr-py.svg)](https://pypi.org/project/seedr-py/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A modern, high-speed Python client and API wrapper for **[Seedr.cc](https://www.seedr.cc)**.
8
+
9
+ Designed to overcome the limitations of legacy wrappers: provides **zero-CAPTCHA email/password authentication**, **one-click full folder ZIP downloads without mCaptcha**, **browser-compatible HLS video streaming**, and clean dataclass models.
10
+
11
+ ---
12
+
13
+ ## ✨ Features
14
+
15
+ - 🚀 **Zero-CAPTCHA Login:** Authenticates programmatically via Seedr's internal identity handshake (`seedr_sbvt`). No browser automation or CAPTCHA solving required.
16
+ - 📦 **One-Click Folder ZIP Downloads:** Download entire subfolder structures as a single ZIP archive directly from Seedr CDN with zero mCaptcha prompts.
17
+ - 🎬 **HLS Video Streaming:** Access both raw 1080p/4K bitrate files and browser-compatible HLS (`.m3u8`) streams with auto-converted AAC stereo audio (fixing silent Dolby EAC3 audio in Chrome/Firefox).
18
+ - 🧲 **Magnets & `.torrent` Files:** Supports magnet links, direct torrent URLs, and local `.torrent` file uploads.
19
+ - 📂 **Full Cloud File Management:** List directories, inspect quotas, retrieve direct CDN download links, cancel downloading tasks, and batch delete.
20
+ - 🛡️ **Fully Typed Dataclasses:** Every response returns rich, typed models (`FolderContents`, `SeedrFile`, `Quota`, `AccountSettings`, `VideoStream`).
21
+
22
+ ---
23
+
24
+ ## 📦 Installation
25
+
26
+ ```bash
27
+ pip install seedr-py
28
+ ```
29
+
30
+ ---
31
+
32
+ ## ⚡ Quickstart
33
+
34
+ ### 1. Authenticate
35
+
36
+ ```python
37
+ from seedr_py import SeedrClient
38
+
39
+ # Login with email and password (Zero CAPTCHA)
40
+ client = SeedrClient.from_password("your_email@example.com", "your_password")
41
+
42
+ # Or authenticate using cookies
43
+ client = SeedrClient.from_cookies({
44
+ "seedr_ptok": "your_token",
45
+ "RSESS_remember": "your_session"
46
+ })
47
+ ```
48
+
49
+ ### 2. Check Quota & Account
50
+
51
+ ```python
52
+ quota = client.get_quota()
53
+ print(f"Storage: {quota.space_used_formatted} / {quota.space_max_formatted} ({quota.space_pct}%)")
54
+
55
+ account = client.get_account_settings()
56
+ print(f"Logged in as: {account.email} (User ID: {account.user_id})")
57
+ ```
58
+
59
+ ### 3. Browse Files & Folders
60
+
61
+ ```python
62
+ # folder_id=0 represents the root directory
63
+ root = client.get_folder(0)
64
+
65
+ # List subfolders
66
+ for folder in root.folders:
67
+ print(f"📁 {folder.name} (ID: {folder.id}, Size: {folder.size_formatted})")
68
+
69
+ # List files
70
+ for file in root.files:
71
+ print(f"📄 {file.name} (ID: {file.id}, Size: {file.size_formatted})")
72
+
73
+ # List active torrent transfers
74
+ for task in root.tasks:
75
+ print(f"⚡ {task.name}: {task.progress}% @ {task.download_rate / 1024:.1f} KB/s")
76
+ ```
77
+
78
+ ### 4. Download Entire Folder as a ZIP (No CAPTCHA)
79
+
80
+ ```python
81
+ # 1. Get the direct signed ZIP URL from Seedr CDN
82
+ archive = client.get_folder_zip_url(folder_id=1438069680)
83
+ print("Signed ZIP Download Link:", archive.url)
84
+
85
+ # 2. Or stream it directly to disk
86
+ client.download_folder_zip(folder_id=1438069680, save_path="MyFolder.zip")
87
+ ```
88
+
89
+ ### 5. Add Torrents & Magnet Links
90
+
91
+ ```python
92
+ # Add a magnet link
93
+ task = client.add_magnet("magnet:?xt=urn:btih:08ada5a7a6183aae1e0908bc1376f4c61b03387c...")
94
+ print(f"Transfer queued! Torrent ID: {task.get('user_torrent_id')}")
95
+
96
+ # Or upload a local .torrent file
97
+ client.add_torrent_file("ubuntu-24.04.torrent")
98
+ ```
99
+
100
+ ### 6. Stream Videos with Browser-Compatible Audio
101
+
102
+ ```python
103
+ stream = client.get_video_stream(file_id=6018680430)
104
+
105
+ # Universal HLS stream with converted AAC audio
106
+ print("HLS Stream URL:", stream.hls_url)
107
+
108
+ # Raw untouched 1080p/4K bitrate stream (for VLC / external players)
109
+ print("Raw Stream URL:", stream.raw_url)
110
+
111
+ # Multi-language subtitle tracks (.vtt)
112
+ print("Subtitles:", stream.subtitles)
113
+ ```
114
+
115
+ ### 7. Delete Files, Folders, or Tasks
116
+
117
+ ```python
118
+ # Cancel an active downloading torrent
119
+ client.cancel_task(task_id=200392405)
120
+
121
+ # Delete a folder
122
+ client.delete_folder(folder_id=1438069680)
123
+
124
+ # Delete a file
125
+ client.delete_file(file_id=6018680430)
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 📜 License
131
+
132
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "seedr-py"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Nishant", email = "nishantprime@gmail.com" },
10
+ ]
11
+ description = "Modern Python client and API wrapper for Seedr.cc with zero-CAPTCHA authentication, folder ZIP streaming, and HLS playback."
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ requires-python = ">=3.8"
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Operating System :: OS Independent",
24
+ "Topic :: Internet :: WWW/HTTP",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.28.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ "Homepage" = "https://github.com/nishantprime/seedr-py"
33
+ "Bug Tracker" = "https://github.com/nishantprime/seedr-py/issues"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,45 @@
1
+ """
2
+ seedr-py: A modern, reverse-engineered Python wrapper for Seedr.cc
3
+ """
4
+
5
+ from seedr_py.client import SeedrClient
6
+ from seedr_py.models import (
7
+ AccountSettings,
8
+ Quota,
9
+ SeedrFile,
10
+ SubFolder,
11
+ Task,
12
+ FolderContents,
13
+ DownloadLink,
14
+ FolderArchive,
15
+ VideoStream
16
+ )
17
+ from seedr_py.exceptions import (
18
+ SeedrError,
19
+ AuthenticationError,
20
+ APIError,
21
+ TorrentError,
22
+ NotFoundError
23
+ )
24
+ from seedr_py.utils import format_bytes, torrent_to_magnet
25
+
26
+ __version__ = "0.1.0"
27
+ __all__ = [
28
+ "SeedrClient",
29
+ "AccountSettings",
30
+ "Quota",
31
+ "SeedrFile",
32
+ "SubFolder",
33
+ "Task",
34
+ "FolderContents",
35
+ "DownloadLink",
36
+ "FolderArchive",
37
+ "VideoStream",
38
+ "SeedrError",
39
+ "AuthenticationError",
40
+ "APIError",
41
+ "TorrentError",
42
+ "NotFoundError",
43
+ "format_bytes",
44
+ "torrent_to_magnet",
45
+ ]
@@ -0,0 +1,352 @@
1
+ """
2
+ Core Seedr Client implementation.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import time
8
+ import requests
9
+ from typing import Dict, Any, List, Optional, Union
10
+
11
+ from seedr_py.models import (
12
+ AccountSettings,
13
+ Quota,
14
+ SeedrFile,
15
+ SubFolder,
16
+ Task,
17
+ FolderContents,
18
+ DownloadLink,
19
+ FolderArchive,
20
+ VideoStream
21
+ )
22
+ from seedr_py.exceptions import (
23
+ AuthenticationError,
24
+ APIError,
25
+ TorrentError,
26
+ NotFoundError
27
+ )
28
+ from seedr_py.utils import torrent_to_magnet
29
+
30
+ class SeedrClient:
31
+ """
32
+ Python client for Seedr.cc supporting:
33
+ - Zero-CAPTCHA email/password authentication
34
+ - Zero-CAPTCHA full folder ZIP archive generation
35
+ - Browser-compatible HLS video streaming
36
+ - Direct CDN signed file download links
37
+ - Magnet links and .torrent file uploads
38
+ """
39
+ BASE_URL = "https://www.seedr.cc/api/v0.1"
40
+
41
+ def __init__(self, session: Optional[requests.Session] = None, cookies: Optional[Dict[str, str]] = None):
42
+ self.session = session or requests.Session()
43
+ self.session.headers.update({
44
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
45
+ "Accept": "application/json, text/plain, */*",
46
+ "Referer": "https://www.seedr.cc/files",
47
+ })
48
+ self._csrf_token: Optional[str] = None
49
+
50
+ if cookies:
51
+ for name, value in cookies.items():
52
+ self.session.cookies.set(name, value, domain=".seedr.cc")
53
+
54
+ def __enter__(self) -> "SeedrClient":
55
+ return self
56
+
57
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
58
+ self.close()
59
+
60
+ def close(self) -> None:
61
+ """Close the underlying HTTP session."""
62
+ if self.session:
63
+ self.session.close()
64
+
65
+ @classmethod
66
+ def from_password(cls, email: str, password: str) -> "SeedrClient":
67
+ """
68
+ Authenticate directly using Email and Password via Seedr's internal
69
+ identity proof token (seedr_sbvt) handshake, completely bypassing
70
+ Cloudflare Turnstile and hCaptcha.
71
+ """
72
+ session = requests.Session()
73
+ session.headers.update({
74
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
75
+ "Accept": "application/json, text/plain, */*",
76
+ "Referer": "https://www.seedr.cc/files",
77
+ })
78
+
79
+ # Step 1: Handshake for bind token (sbvt)
80
+ try:
81
+ ptok_resp = session.get(f"{cls.BASE_URL}/auth/ptok", timeout=12)
82
+ ptok_resp.raise_for_status()
83
+ sbvt = ptok_resp.json().get("sbvt")
84
+ except Exception as e:
85
+ raise AuthenticationError(f"Failed to obtain identity proof handshake: {e}") from e
86
+
87
+ # Step 2: Login request
88
+ login_payload = {
89
+ "username": email,
90
+ "password": password,
91
+ "rememberme": 1,
92
+ "seedr_sbvt": sbvt
93
+ }
94
+ try:
95
+ resp = session.post(f"{cls.BASE_URL}/auth/login", json=login_payload, timeout=15)
96
+ data = resp.json()
97
+ except Exception as e:
98
+ raise AuthenticationError(f"Login request failed: {e}") from e
99
+
100
+ if resp.status_code != 200 or not data.get("success"):
101
+ reason = data.get("reason_phrase") or data.get("error") or "Authentication failed"
102
+ raise AuthenticationError(f"Invalid credentials ({resp.status_code}): {reason}")
103
+
104
+ # Step 3: Populate any additional cookie strings from response payload
105
+ if "cookies" in data:
106
+ for c_str in data["cookies"]:
107
+ parts = c_str.split(";")[0].split("=", 1)
108
+ if len(parts) == 2:
109
+ session.cookies.set(parts[0].strip(), parts[1].strip(), domain=".seedr.cc")
110
+
111
+ return cls(session=session)
112
+
113
+ @classmethod
114
+ def from_cookies(cls, cookies: Dict[str, str]) -> "SeedrClient":
115
+ """
116
+ Authenticate using pre-existing browser cookies (e.g. seedr_ptok or RSESS_remember).
117
+ """
118
+ return cls(cookies=cookies)
119
+
120
+ def _get_csrf_token(self, refresh: bool = False) -> str:
121
+ """Fetch and cache CSRF token required for mutating API actions."""
122
+ if not self._csrf_token or refresh:
123
+ url = f"{self.BASE_URL}/organization/csrf-token"
124
+ resp = self.session.get(url, timeout=12)
125
+ resp.raise_for_status()
126
+ self._csrf_token = resp.json().get("csrf_token")
127
+ self.session.headers["X-CSRF-Token"] = self._csrf_token
128
+ return self._csrf_token
129
+
130
+ def get_account_settings(self) -> AccountSettings:
131
+ """Fetch user profile details (email, user_id, storage quota, package type)."""
132
+ url = f"{self.BASE_URL}/account/settings"
133
+ resp = self.session.get(url, timeout=15)
134
+ if resp.status_code in (401, 403):
135
+ raise AuthenticationError("Session expired or unauthorized.")
136
+ resp.raise_for_status()
137
+ data = resp.json()
138
+ acc = data.get("account", {})
139
+ return AccountSettings(
140
+ user_id=acc.get("user_id", 0),
141
+ email=acc.get("email", ""),
142
+ username=acc.get("username", ""),
143
+ is_premium=acc.get("premium", False),
144
+ package_name=acc.get("package_name", "Free"),
145
+ space_used=acc.get("space_used", 0),
146
+ space_max=acc.get("space_max", 0),
147
+ country=data.get("country", "")
148
+ )
149
+
150
+ def get_quota(self) -> Quota:
151
+ """Fetch storage space and bandwidth quota information."""
152
+ url = f"{self.BASE_URL}/account/quota/used"
153
+ resp = self.session.get(url, timeout=12)
154
+ if resp.status_code in (401, 403):
155
+ raise AuthenticationError("Session expired or unauthorized.")
156
+ resp.raise_for_status()
157
+ data = resp.json()
158
+ return Quota(
159
+ space_used=data.get("space_used", 0),
160
+ space_max=data.get("space_max", 0),
161
+ bandwidth_used=data.get("bandwidth_used", 0),
162
+ is_premium=data.get("is_premium", False)
163
+ )
164
+
165
+ def get_folder(self, folder_id: int = 0) -> FolderContents:
166
+ """
167
+ Fetch subfolders, files, and active torrent tasks inside a directory.
168
+ folder_id = 0 represents the root directory.
169
+ """
170
+ url = f"{self.BASE_URL}/fs/folder/{folder_id}/items"
171
+ resp = self.session.get(url, timeout=15)
172
+ if resp.status_code in (401, 403):
173
+ raise AuthenticationError("Session expired or unauthorized.")
174
+ if resp.status_code == 404:
175
+ raise NotFoundError(f"Folder ID {folder_id} not found.")
176
+ resp.raise_for_status()
177
+ data = resp.json()
178
+
179
+ folders = [
180
+ SubFolder(
181
+ id=fo.get("id"),
182
+ name=fo.get("path"),
183
+ size=fo.get("size", 0),
184
+ last_update=fo.get("last_update", "")
185
+ )
186
+ for fo in data.get("folders", [])
187
+ ]
188
+
189
+ files = [
190
+ SeedrFile(
191
+ id=fi.get("id"),
192
+ name=fi.get("name"),
193
+ size=fi.get("size", 0),
194
+ hash=fi.get("hash", ""),
195
+ folder_id=fi.get("folder_id", folder_id),
196
+ is_video=fi.get("is_video", False),
197
+ is_audio=fi.get("is_audio", False),
198
+ thumb=fi.get("thumb", "")
199
+ )
200
+ for fi in data.get("files", [])
201
+ ]
202
+
203
+ tasks = [
204
+ Task(
205
+ id=t.get("id"),
206
+ name=t.get("name", ""),
207
+ size=t.get("size", 0),
208
+ progress=t.get("progress", 0.0),
209
+ download_rate=t.get("download_rate", 0),
210
+ seeders=t.get("seeders", 0),
211
+ leechers=t.get("leechers", 0)
212
+ )
213
+ for t in (data.get("torrents", []) or data.get("tasks", []))
214
+ ]
215
+
216
+ return FolderContents(
217
+ id=data.get("id", folder_id),
218
+ path=data.get("path", "Home"),
219
+ parent_id=data.get("parent", -1),
220
+ folders=folders,
221
+ files=files,
222
+ tasks=tasks
223
+ )
224
+
225
+ def get_file_url(self, file_id: int) -> DownloadLink:
226
+ """Generate a signed high-speed CDN direct download link for a file."""
227
+ url = f"{self.BASE_URL}/download/file/{file_id}/url"
228
+ resp = self.session.get(url, timeout=12)
229
+ if resp.status_code == 404:
230
+ raise NotFoundError(f"File ID {file_id} not found.")
231
+ resp.raise_for_status()
232
+ data = resp.json()
233
+ return DownloadLink(url=data.get("url"), name=data.get("name", ""))
234
+
235
+ def get_folder_zip_url(self, folder_id: int) -> FolderArchive:
236
+ """
237
+ Generate a signed direct ZIP archive URL for an entire folder without CAPTCHA.
238
+ Uses POST /api/v0.1/download/archive with X-CSRF-Token.
239
+ """
240
+ self._get_csrf_token()
241
+ url = f"{self.BASE_URL}/download/archive"
242
+ payload = {
243
+ "archive_arr": json.dumps([{"type": "folder", "id": folder_id}])
244
+ }
245
+ resp = self.session.post(url, data=payload, timeout=15)
246
+ resp.raise_for_status()
247
+ data = resp.json()
248
+ return FolderArchive(
249
+ archive_id=str(data.get("archive_id")),
250
+ url=data.get("url")
251
+ )
252
+
253
+ def download_folder_zip(self, folder_id: int, save_path: str, chunk_size: int = 128 * 1024) -> str:
254
+ """
255
+ Streams and saves an entire folder as a ZIP file directly to local disk.
256
+ """
257
+ archive = self.get_folder_zip_url(folder_id)
258
+ with requests.get(archive.url, stream=True, timeout=30) as r:
259
+ r.raise_for_status()
260
+ with open(save_path, "wb") as f:
261
+ for chunk in r.iter_content(chunk_size=chunk_size):
262
+ if chunk:
263
+ f.write(chunk)
264
+ return save_path
265
+
266
+ def get_video_stream(self, file_id: int) -> VideoStream:
267
+ """
268
+ Fetches streaming endpoints for a video file:
269
+ - raw_url: Direct untouched original bitrate file (1080p/4K)
270
+ - hls_url: Browser-compatible HLS stream with converted AAC audio
271
+ - subtitles: WebVTT multi-language subtitle tracks
272
+ """
273
+ dl = self.get_file_url(file_id)
274
+ hls_url = None
275
+ subtitles = {}
276
+
277
+ try:
278
+ resp = self.session.get(f"{self.BASE_URL}/media/hls/{file_id}", timeout=12)
279
+ if resp.status_code == 200:
280
+ h_data = resp.json()
281
+ hls_url = h_data.get("url")
282
+ subtitles = h_data.get("subtitles", {})
283
+ except Exception:
284
+ pass
285
+
286
+ return VideoStream(
287
+ file_id=file_id,
288
+ name=dl.name,
289
+ hls_url=hls_url,
290
+ raw_url=dl.url,
291
+ subtitles=subtitles
292
+ )
293
+
294
+ def add_magnet(self, magnet_link: str, folder_id: int = 0) -> Dict[str, Any]:
295
+ """Add a magnet link download to the account."""
296
+ self._get_csrf_token()
297
+ url = f"{self.BASE_URL}/task"
298
+ payload = {
299
+ "folder_id": folder_id,
300
+ "type": "torrent",
301
+ "torrent_magnet": magnet_link.strip()
302
+ }
303
+ resp = self.session.post(url, data=payload, timeout=15)
304
+ data = resp.json()
305
+ if not data.get("success"):
306
+ reason = data.get("reason_phrase") or "Failed to add magnet link."
307
+ raise TorrentError(reason)
308
+ return data
309
+
310
+ def add_torrent_file(self, torrent_input: Union[str, bytes], folder_id: int = 0) -> Dict[str, Any]:
311
+ """
312
+ Add a .torrent file to the account.
313
+ Accepts either a file path (str) or raw bytes.
314
+ """
315
+ if isinstance(torrent_input, str):
316
+ with open(torrent_input, "rb") as f:
317
+ torrent_bytes = f.read()
318
+ else:
319
+ torrent_bytes = torrent_input
320
+
321
+ magnet = torrent_to_magnet(torrent_bytes)
322
+ return self.add_magnet(magnet, folder_id=folder_id)
323
+
324
+ def cancel_task(self, task_id: int) -> bool:
325
+ """Cancel and delete an in-progress torrent task."""
326
+ self._get_csrf_token()
327
+ url = f"{self.BASE_URL}/tasks/{task_id}"
328
+ resp = self.session.delete(url, timeout=12)
329
+ resp.raise_for_status()
330
+ return resp.json().get("success", False)
331
+
332
+ def delete_folder(self, folder_id: int) -> bool:
333
+ """Delete a folder and its contents."""
334
+ return self.delete_batch([{"type": "folder", "id": folder_id}])
335
+
336
+ def delete_file(self, file_id: int) -> bool:
337
+ """Delete an individual file."""
338
+ return self.delete_batch([{"type": "file", "id": file_id}])
339
+
340
+ def delete_batch(self, items: List[Dict[str, Any]]) -> bool:
341
+ """
342
+ Batch delete folders and files.
343
+ items format: [{"type": "folder", "id": 123}, {"type": "file", "id": 456}]
344
+ """
345
+ self._get_csrf_token()
346
+ url = f"{self.BASE_URL}/fs/batch/delete"
347
+ payload = {
348
+ "delete_arr": json.dumps(items)
349
+ }
350
+ resp = self.session.post(url, data=payload, timeout=15)
351
+ resp.raise_for_status()
352
+ return resp.json().get("success", False)
@@ -0,0 +1,26 @@
1
+ """
2
+ Custom exceptions for seedr-py
3
+ """
4
+
5
+ class SeedrError(Exception):
6
+ """Base exception for all Seedr errors."""
7
+ pass
8
+
9
+ class AuthenticationError(SeedrError):
10
+ """Raised when authentication fails (invalid credentials or expired session)."""
11
+ pass
12
+
13
+ class APIError(SeedrError):
14
+ """Raised when the Seedr API returns an error response."""
15
+ def __init__(self, message: str, status_code: int = None, response_data: dict = None):
16
+ super().__init__(message)
17
+ self.status_code = status_code
18
+ self.response_data = response_data
19
+
20
+ class TorrentError(SeedrError):
21
+ """Raised when adding a magnet link or torrent fails."""
22
+ pass
23
+
24
+ class NotFoundError(SeedrError):
25
+ """Raised when a requested file or folder is not found."""
26
+ pass
@@ -0,0 +1,116 @@
1
+ """
2
+ Dataclass models representing Seedr resources.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import List, Dict, Any, Optional
7
+ from seedr_py.utils import format_bytes
8
+
9
+ @dataclass
10
+ class AccountSettings:
11
+ user_id: int
12
+ email: str
13
+ username: str
14
+ is_premium: bool
15
+ package_name: str
16
+ space_used: int
17
+ space_max: int
18
+ country: str = ""
19
+
20
+ @property
21
+ def space_used_formatted(self) -> str:
22
+ return format_bytes(self.space_used)
23
+
24
+ @property
25
+ def space_max_formatted(self) -> str:
26
+ return format_bytes(self.space_max)
27
+
28
+ @property
29
+ def space_pct(self) -> float:
30
+ return round((self.space_used / self.space_max) * 100, 1) if self.space_max > 0 else 0.0
31
+
32
+ @dataclass
33
+ class Quota:
34
+ space_used: int
35
+ space_max: int
36
+ bandwidth_used: int
37
+ is_premium: bool
38
+
39
+ @property
40
+ def space_used_formatted(self) -> str:
41
+ return format_bytes(self.space_used)
42
+
43
+ @property
44
+ def space_max_formatted(self) -> str:
45
+ return format_bytes(self.space_max)
46
+
47
+ @property
48
+ def space_pct(self) -> float:
49
+ return round((self.space_used / self.space_max) * 100, 1) if self.space_max > 0 else 0.0
50
+
51
+ @dataclass
52
+ class SeedrFile:
53
+ id: int
54
+ name: str
55
+ size: int
56
+ hash: str
57
+ folder_id: int
58
+ is_video: bool = False
59
+ is_audio: bool = False
60
+ thumb: str = ""
61
+
62
+ @property
63
+ def size_formatted(self) -> str:
64
+ return format_bytes(self.size)
65
+
66
+ @dataclass
67
+ class SubFolder:
68
+ id: int
69
+ name: str
70
+ size: int
71
+ last_update: str = ""
72
+
73
+ @property
74
+ def size_formatted(self) -> str:
75
+ return format_bytes(self.size)
76
+
77
+ @dataclass
78
+ class Task:
79
+ id: int
80
+ name: str
81
+ size: int
82
+ progress: float
83
+ download_rate: int
84
+ seeders: int = 0
85
+ leechers: int = 0
86
+
87
+ @property
88
+ def size_formatted(self) -> str:
89
+ return format_bytes(self.size)
90
+
91
+ @dataclass
92
+ class FolderContents:
93
+ id: int
94
+ path: str
95
+ parent_id: int
96
+ folders: List[SubFolder] = field(default_factory=list)
97
+ files: List[SeedrFile] = field(default_factory=list)
98
+ tasks: List[Task] = field(default_factory=list)
99
+
100
+ @dataclass
101
+ class DownloadLink:
102
+ url: str
103
+ name: str
104
+
105
+ @dataclass
106
+ class FolderArchive:
107
+ archive_id: str
108
+ url: str
109
+
110
+ @dataclass
111
+ class VideoStream:
112
+ file_id: int
113
+ name: str
114
+ hls_url: Optional[str]
115
+ raw_url: str
116
+ subtitles: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,92 @@
1
+ """
2
+ Utility functions for torrent parsing and formatting.
3
+ """
4
+
5
+ import hashlib
6
+ from typing import Dict, Any, Tuple
7
+
8
+ def format_bytes(num_bytes: int) -> str:
9
+ """Convert bytes count to human-readable string (e.g. 1.25 GB)."""
10
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
11
+ if num_bytes < 1024.0:
12
+ return f"{num_bytes:.2f} {unit}"
13
+ num_bytes /= 1024.0
14
+ return f"{num_bytes:.2f} PB"
15
+
16
+ def bdecode(data: bytes) -> Any:
17
+ """Minimal, pure-Python bencode decoder for .torrent files."""
18
+ def decode_item(index: int) -> Tuple[Any, int]:
19
+ char = chr(data[index])
20
+ if char == 'i':
21
+ end = data.index(b'e', index)
22
+ return int(data[index + 1:end]), end + 1
23
+ elif char.isdigit():
24
+ colon = data.index(b':', index)
25
+ length = int(data[index:colon])
26
+ start = colon + 1
27
+ return data[start:start + length], start + length
28
+ elif char == 'l':
29
+ items = []
30
+ idx = index + 1
31
+ while chr(data[idx]) != 'e':
32
+ item, idx = decode_item(idx)
33
+ items.append(item)
34
+ return items, idx + 1
35
+ elif char == 'd':
36
+ items = {}
37
+ idx = index + 1
38
+ while chr(data[idx]) != 'e':
39
+ key, idx = decode_item(idx)
40
+ val, idx = decode_item(idx)
41
+ items[key.decode('utf-8', errors='ignore')] = val
42
+ return items, idx + 1
43
+ raise ValueError(f"Unknown bencode type at offset {index}: {char}")
44
+
45
+ return decode_item(0)[0]
46
+
47
+ def torrent_to_magnet(torrent_bytes: bytes) -> str:
48
+ """
49
+ Extracts info-hash from raw .torrent file bytes and converts to magnet link.
50
+ """
51
+ info_start = torrent_bytes.find(b'4:info') + 6
52
+ if info_start < 6:
53
+ raise ValueError("Invalid .torrent file: 'info' dictionary not found.")
54
+
55
+ def find_end_dict(b: bytes, start: int) -> int:
56
+ depth = 0
57
+ i = start
58
+ while i < len(b):
59
+ c = chr(b[i])
60
+ if c in ('d', 'l'):
61
+ depth += 1
62
+ i += 1
63
+ elif c == 'i':
64
+ i = b.index(b'e', i) + 1
65
+ elif c.isdigit():
66
+ colon = b.index(b':', i)
67
+ length = int(b[i:colon])
68
+ i = colon + 1 + length
69
+ elif c == 'e':
70
+ depth -= 1
71
+ i += 1
72
+ if depth == 0:
73
+ return i
74
+ else:
75
+ i += 1
76
+ return len(b)
77
+
78
+ info_end = find_end_dict(torrent_bytes, info_start)
79
+ raw_info = torrent_bytes[info_start:info_end]
80
+ sha1 = hashlib.sha1(raw_info).hexdigest()
81
+
82
+ name = "torrent"
83
+ try:
84
+ decoded = bdecode(torrent_bytes)
85
+ info_d = decoded.get('info', {})
86
+ if 'name' in info_d:
87
+ val = info_d['name']
88
+ name = val.decode('utf-8', errors='ignore') if isinstance(val, bytes) else str(val)
89
+ except Exception:
90
+ pass
91
+
92
+ return f"magnet:?xt=urn:btih:{sha1}&dn={name}"
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: seedr-py
3
+ Version: 0.1.0
4
+ Summary: Modern Python client and API wrapper for Seedr.cc with zero-CAPTCHA authentication, folder ZIP streaming, and HLS playback.
5
+ Author-email: Nishant <nishantprime@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nishantprime/seedr-py
8
+ Project-URL: Bug Tracker, https://github.com/nishantprime/seedr-py/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.28.0
22
+ Dynamic: license-file
23
+
24
+ # seedr-py ⚡
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/seedr-py.svg)](https://pypi.org/project/seedr-py/)
27
+ [![Python versions](https://img.shields.io/pypi/pyversions/seedr-py.svg)](https://pypi.org/project/seedr-py/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
29
+
30
+ A modern, high-speed Python client and API wrapper for **[Seedr.cc](https://www.seedr.cc)**.
31
+
32
+ Designed to overcome the limitations of legacy wrappers: provides **zero-CAPTCHA email/password authentication**, **one-click full folder ZIP downloads without mCaptcha**, **browser-compatible HLS video streaming**, and clean dataclass models.
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+
38
+ - 🚀 **Zero-CAPTCHA Login:** Authenticates programmatically via Seedr's internal identity handshake (`seedr_sbvt`). No browser automation or CAPTCHA solving required.
39
+ - 📦 **One-Click Folder ZIP Downloads:** Download entire subfolder structures as a single ZIP archive directly from Seedr CDN with zero mCaptcha prompts.
40
+ - 🎬 **HLS Video Streaming:** Access both raw 1080p/4K bitrate files and browser-compatible HLS (`.m3u8`) streams with auto-converted AAC stereo audio (fixing silent Dolby EAC3 audio in Chrome/Firefox).
41
+ - 🧲 **Magnets & `.torrent` Files:** Supports magnet links, direct torrent URLs, and local `.torrent` file uploads.
42
+ - 📂 **Full Cloud File Management:** List directories, inspect quotas, retrieve direct CDN download links, cancel downloading tasks, and batch delete.
43
+ - 🛡️ **Fully Typed Dataclasses:** Every response returns rich, typed models (`FolderContents`, `SeedrFile`, `Quota`, `AccountSettings`, `VideoStream`).
44
+
45
+ ---
46
+
47
+ ## 📦 Installation
48
+
49
+ ```bash
50
+ pip install seedr-py
51
+ ```
52
+
53
+ ---
54
+
55
+ ## ⚡ Quickstart
56
+
57
+ ### 1. Authenticate
58
+
59
+ ```python
60
+ from seedr_py import SeedrClient
61
+
62
+ # Login with email and password (Zero CAPTCHA)
63
+ client = SeedrClient.from_password("your_email@example.com", "your_password")
64
+
65
+ # Or authenticate using cookies
66
+ client = SeedrClient.from_cookies({
67
+ "seedr_ptok": "your_token",
68
+ "RSESS_remember": "your_session"
69
+ })
70
+ ```
71
+
72
+ ### 2. Check Quota & Account
73
+
74
+ ```python
75
+ quota = client.get_quota()
76
+ print(f"Storage: {quota.space_used_formatted} / {quota.space_max_formatted} ({quota.space_pct}%)")
77
+
78
+ account = client.get_account_settings()
79
+ print(f"Logged in as: {account.email} (User ID: {account.user_id})")
80
+ ```
81
+
82
+ ### 3. Browse Files & Folders
83
+
84
+ ```python
85
+ # folder_id=0 represents the root directory
86
+ root = client.get_folder(0)
87
+
88
+ # List subfolders
89
+ for folder in root.folders:
90
+ print(f"📁 {folder.name} (ID: {folder.id}, Size: {folder.size_formatted})")
91
+
92
+ # List files
93
+ for file in root.files:
94
+ print(f"📄 {file.name} (ID: {file.id}, Size: {file.size_formatted})")
95
+
96
+ # List active torrent transfers
97
+ for task in root.tasks:
98
+ print(f"⚡ {task.name}: {task.progress}% @ {task.download_rate / 1024:.1f} KB/s")
99
+ ```
100
+
101
+ ### 4. Download Entire Folder as a ZIP (No CAPTCHA)
102
+
103
+ ```python
104
+ # 1. Get the direct signed ZIP URL from Seedr CDN
105
+ archive = client.get_folder_zip_url(folder_id=1438069680)
106
+ print("Signed ZIP Download Link:", archive.url)
107
+
108
+ # 2. Or stream it directly to disk
109
+ client.download_folder_zip(folder_id=1438069680, save_path="MyFolder.zip")
110
+ ```
111
+
112
+ ### 5. Add Torrents & Magnet Links
113
+
114
+ ```python
115
+ # Add a magnet link
116
+ task = client.add_magnet("magnet:?xt=urn:btih:08ada5a7a6183aae1e0908bc1376f4c61b03387c...")
117
+ print(f"Transfer queued! Torrent ID: {task.get('user_torrent_id')}")
118
+
119
+ # Or upload a local .torrent file
120
+ client.add_torrent_file("ubuntu-24.04.torrent")
121
+ ```
122
+
123
+ ### 6. Stream Videos with Browser-Compatible Audio
124
+
125
+ ```python
126
+ stream = client.get_video_stream(file_id=6018680430)
127
+
128
+ # Universal HLS stream with converted AAC audio
129
+ print("HLS Stream URL:", stream.hls_url)
130
+
131
+ # Raw untouched 1080p/4K bitrate stream (for VLC / external players)
132
+ print("Raw Stream URL:", stream.raw_url)
133
+
134
+ # Multi-language subtitle tracks (.vtt)
135
+ print("Subtitles:", stream.subtitles)
136
+ ```
137
+
138
+ ### 7. Delete Files, Folders, or Tasks
139
+
140
+ ```python
141
+ # Cancel an active downloading torrent
142
+ client.cancel_task(task_id=200392405)
143
+
144
+ # Delete a folder
145
+ client.delete_folder(folder_id=1438069680)
146
+
147
+ # Delete a file
148
+ client.delete_file(file_id=6018680430)
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 📜 License
154
+
155
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/seedr_py/__init__.py
5
+ src/seedr_py/client.py
6
+ src/seedr_py/exceptions.py
7
+ src/seedr_py/models.py
8
+ src/seedr_py/utils.py
9
+ src/seedr_py.egg-info/PKG-INFO
10
+ src/seedr_py.egg-info/SOURCES.txt
11
+ src/seedr_py.egg-info/dependency_links.txt
12
+ src/seedr_py.egg-info/requires.txt
13
+ src/seedr_py.egg-info/top_level.txt
14
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ requests>=2.28.0
@@ -0,0 +1 @@
1
+ seedr_py
@@ -0,0 +1,12 @@
1
+ import pytest
2
+ from seedr_py import SeedrClient, format_bytes, torrent_to_magnet
3
+ from seedr_py.exceptions import AuthenticationError
4
+
5
+ def test_format_bytes():
6
+ assert format_bytes(500) == "500.00 B"
7
+ assert format_bytes(1024 * 1024) == "1.00 MB"
8
+ assert format_bytes(1024 * 1024 * 1024 * 2) == "2.00 GB"
9
+
10
+ def test_invalid_login():
11
+ with pytest.raises(AuthenticationError):
12
+ SeedrClient.from_password("invalid@example.com", "wrongpass123")