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 +21 -0
- seedr_py-0.1.0/PKG-INFO +155 -0
- seedr_py-0.1.0/README.md +132 -0
- seedr_py-0.1.0/pyproject.toml +36 -0
- seedr_py-0.1.0/setup.cfg +4 -0
- seedr_py-0.1.0/src/seedr_py/__init__.py +45 -0
- seedr_py-0.1.0/src/seedr_py/client.py +352 -0
- seedr_py-0.1.0/src/seedr_py/exceptions.py +26 -0
- seedr_py-0.1.0/src/seedr_py/models.py +116 -0
- seedr_py-0.1.0/src/seedr_py/utils.py +92 -0
- seedr_py-0.1.0/src/seedr_py.egg-info/PKG-INFO +155 -0
- seedr_py-0.1.0/src/seedr_py.egg-info/SOURCES.txt +14 -0
- seedr_py-0.1.0/src/seedr_py.egg-info/dependency_links.txt +1 -0
- seedr_py-0.1.0/src/seedr_py.egg-info/requires.txt +1 -0
- seedr_py-0.1.0/src/seedr_py.egg-info/top_level.txt +1 -0
- seedr_py-0.1.0/tests/test_client.py +12 -0
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.
|
seedr_py-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/seedr-py/)
|
|
27
|
+
[](https://pypi.org/project/seedr-py/)
|
|
28
|
+
[](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.
|
seedr_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# seedr-py ⚡
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/seedr-py/)
|
|
4
|
+
[](https://pypi.org/project/seedr-py/)
|
|
5
|
+
[](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"]
|
seedr_py-0.1.0/setup.cfg
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/seedr-py/)
|
|
27
|
+
[](https://pypi.org/project/seedr-py/)
|
|
28
|
+
[](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
|
+
|
|
@@ -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")
|