megaloader-cli 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.
- megaloader_cli-0.1.0/.gitignore +15 -0
- megaloader_cli-0.1.0/MANIFEST.in +2 -0
- megaloader_cli-0.1.0/PKG-INFO +148 -0
- megaloader_cli-0.1.0/megaloader_cli/__init__.py +1 -0
- megaloader_cli-0.1.0/megaloader_cli/commands.py +220 -0
- megaloader_cli-0.1.0/megaloader_cli/io.py +75 -0
- megaloader_cli-0.1.0/megaloader_cli/main.py +95 -0
- megaloader_cli-0.1.0/megaloader_cli/utils.py +50 -0
- megaloader_cli-0.1.0/pyproject.toml +36 -0
- megaloader_cli-0.1.0/readme.md +135 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: megaloader-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for Megaloader
|
|
5
|
+
Maintainer-email: David Duran <dadch1404@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: click>=8.1.0
|
|
9
|
+
Requires-Dist: megaloader
|
|
10
|
+
Requires-Dist: requests>=2.32.0
|
|
11
|
+
Requires-Dist: rich>=13.7.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# [pkg]: megaloader-cli
|
|
15
|
+
|
|
16
|
+
[](https://badge.fury.io/py/megaloader-cli)
|
|
17
|
+
|
|
18
|
+
Command-line interface for the megaloader library. Extract metadata and download
|
|
19
|
+
files from supported hosting platforms directly in your terminal.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install megaloader-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The CLI installs the core megaloader library as a dependency. Both packages can
|
|
28
|
+
be used independently.
|
|
29
|
+
|
|
30
|
+
## Basic usage
|
|
31
|
+
|
|
32
|
+
List available files without downloading:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Download files to a directory:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
List supported platforms:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
megaloader plugins
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
The `extract` command shows file metadata without downloading anything. Add
|
|
53
|
+
`--json` for machine-readable output or `--verbose` for debug logs:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
57
|
+
megaloader extract https://gofile.io/d/xyz456 --json
|
|
58
|
+
megaloader extract https://cyberdrop.me/a/album --verbose
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The `download` command saves files to the specified directory. Defaults to
|
|
62
|
+
`./downloads` when omitted. Files are grouped into collection subfolders by
|
|
63
|
+
default:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
megaloader download https://pixeldrain.com/l/abc123
|
|
67
|
+
megaloader download https://cyberdrop.me/a/album ./my-files
|
|
68
|
+
megaloader download https://bunkr.si/a/xyz789 ./downloads --verbose
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Add `--flat` to disable subfolders and write all files directly to the output
|
|
72
|
+
directory. Use `--filter` to download only files matching a glob pattern:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads --flat
|
|
76
|
+
megaloader download https://cyberdrop.me/a/album ./videos --filter "*.mp4"
|
|
77
|
+
megaloader download https://bunkr.si/a/xyz789 ./images --filter "*.{jpg,png}"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
GoFile password-protected content requires the `--password` argument:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
megaloader download https://gofile.io/d/protected ./downloads --password secret123
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `plugins` command lists all supported platforms and domains.
|
|
87
|
+
|
|
88
|
+
## Common patterns
|
|
89
|
+
|
|
90
|
+
Preview files before downloading:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
94
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Filter by type to download only what you need:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
megaloader download https://cyberdrop.me/a/album ./videos --filter "*.mp4"
|
|
101
|
+
megaloader download https://bunkr.si/a/xyz ./images --filter "*.jpg"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Control file organization with the `--flat` flag:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
megaloader download https://pixeldrain.com/l/abc ./organized
|
|
108
|
+
megaloader download https://pixeldrain.com/l/abc ./flat --flat
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Enable verbose logging for troubleshooting:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
megaloader download https://example.com/file ./downloads --verbose
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Relationship to core library
|
|
118
|
+
|
|
119
|
+
The CLI wraps the core megaloader library. It handles argument parsing, output
|
|
120
|
+
formatting, and file downloads. The core library performs URL detection,
|
|
121
|
+
metadata extraction, and platform-specific logic.
|
|
122
|
+
|
|
123
|
+
Use the CLI for terminal workflows, quick downloads, and platform exploration.
|
|
124
|
+
Use the core library for Python integration, custom handling, progress tracking,
|
|
125
|
+
and batch processing. See the core library documentation for programmatic usage.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
The CLI is part of a uv workspace. Install from the repository root:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
uv sync
|
|
133
|
+
uv run megaloader --help
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Install in editable mode for development:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv pip install -e packages/cli
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Run `uv run ruff format .` and the test suite before committing.
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
Contributions are welcome. See the repository contributing guide for setup and
|
|
147
|
+
submission details. Report bugs and request features through GitHub Discussions.
|
|
148
|
+
Include your Python version, error messages, and problematic URLs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import megaloader as mgl
|
|
9
|
+
|
|
10
|
+
from megaloader.exceptions import MegaloaderError
|
|
11
|
+
from megaloader.plugins import get_plugin_class
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
DownloadColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
TextColumn,
|
|
17
|
+
TimeRemainingColumn,
|
|
18
|
+
TransferSpeedColumn,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from megaloader_cli.io import download_file
|
|
22
|
+
from megaloader_cli.utils import console, sanitize_for_filesystem
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_command(url: str, output_json: bool) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Handle extract command logic.
|
|
28
|
+
|
|
29
|
+
Fetches metadata and displays items without downloading.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# Show which plugin is being used (only in human-readable mode)
|
|
33
|
+
if not output_json and (plugin_name := _get_plugin_name(url)):
|
|
34
|
+
console.print(f"[green]✓[/green] Using plugin: [bold]{plugin_name}[/bold]")
|
|
35
|
+
|
|
36
|
+
# Stream items as they're discovered
|
|
37
|
+
items = []
|
|
38
|
+
if output_json:
|
|
39
|
+
# Silent extraction for JSON mode
|
|
40
|
+
for item in mgl.extract(url):
|
|
41
|
+
items.append(item)
|
|
42
|
+
else:
|
|
43
|
+
# Show progress for human-readable mode
|
|
44
|
+
with Progress(
|
|
45
|
+
TextColumn("[bold blue]Extracting metadata..."),
|
|
46
|
+
BarColumn(),
|
|
47
|
+
console=console,
|
|
48
|
+
) as progress:
|
|
49
|
+
task = progress.add_task("", total=None)
|
|
50
|
+
|
|
51
|
+
for item in mgl.extract(url):
|
|
52
|
+
items.append(item)
|
|
53
|
+
progress.update(task, advance=1)
|
|
54
|
+
|
|
55
|
+
# Display results
|
|
56
|
+
if output_json:
|
|
57
|
+
_print_json(url, items)
|
|
58
|
+
else:
|
|
59
|
+
_print_human_readable(items)
|
|
60
|
+
|
|
61
|
+
except MegaloaderError as e:
|
|
62
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def download_command(
|
|
67
|
+
url: str,
|
|
68
|
+
output_dir: str,
|
|
69
|
+
flat: bool,
|
|
70
|
+
pattern: str | None,
|
|
71
|
+
options: dict[str, Any],
|
|
72
|
+
) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Handle download command logic.
|
|
75
|
+
|
|
76
|
+
Extracts metadata, filters items, and downloads files with progress tracking.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
# Show which plugin is being used
|
|
80
|
+
if plugin_name := _get_plugin_name(url):
|
|
81
|
+
console.print(f"[green]✓[/green] Using plugin: [bold]{plugin_name}[/bold]")
|
|
82
|
+
|
|
83
|
+
# Stream and collect items
|
|
84
|
+
items = []
|
|
85
|
+
with Progress(
|
|
86
|
+
TextColumn("[bold blue]Discovering files..."),
|
|
87
|
+
BarColumn(),
|
|
88
|
+
console=console,
|
|
89
|
+
) as progress:
|
|
90
|
+
task = progress.add_task("", total=None)
|
|
91
|
+
|
|
92
|
+
for item in mgl.extract(url, **options):
|
|
93
|
+
items.append(item)
|
|
94
|
+
progress.update(task, advance=1)
|
|
95
|
+
|
|
96
|
+
# Apply filter if specified
|
|
97
|
+
if pattern:
|
|
98
|
+
original_count = len(items)
|
|
99
|
+
items = [item for item in items if fnmatch(item.filename, pattern)]
|
|
100
|
+
console.print(f"[dim]Filtered: {original_count} → {len(items)} files[/dim]")
|
|
101
|
+
|
|
102
|
+
if not items:
|
|
103
|
+
console.print("[yellow]⚠ No files to download.[/yellow]")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
console.print(f"[green]✓[/green] Found [bold]{len(items)}[/bold] files.")
|
|
107
|
+
|
|
108
|
+
# Download files with progress tracking
|
|
109
|
+
_download_with_progress(items, Path(output_dir), flat)
|
|
110
|
+
|
|
111
|
+
except MegaloaderError as e:
|
|
112
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _download_with_progress(
|
|
117
|
+
items: list[mgl.DownloadItem],
|
|
118
|
+
base_dir: Path,
|
|
119
|
+
flat: bool,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Download all items with rich progress bars.
|
|
123
|
+
|
|
124
|
+
Shows individual file progress and overall completion.
|
|
125
|
+
"""
|
|
126
|
+
success_count = 0
|
|
127
|
+
failed_count = 0
|
|
128
|
+
|
|
129
|
+
progress = Progress(
|
|
130
|
+
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
|
|
131
|
+
BarColumn(bar_width=None),
|
|
132
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
133
|
+
"•",
|
|
134
|
+
DownloadColumn(),
|
|
135
|
+
"•",
|
|
136
|
+
TransferSpeedColumn(),
|
|
137
|
+
"•",
|
|
138
|
+
TimeRemainingColumn(),
|
|
139
|
+
console=console,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
with progress:
|
|
143
|
+
# Overall progress tracker
|
|
144
|
+
overall = progress.add_task(
|
|
145
|
+
"Overall Progress",
|
|
146
|
+
total=len(items),
|
|
147
|
+
filename="Batch",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
for item in items:
|
|
151
|
+
# Determine destination path
|
|
152
|
+
if flat or not item.collection_name:
|
|
153
|
+
dest_dir = base_dir
|
|
154
|
+
else:
|
|
155
|
+
dest_dir = base_dir / sanitize_for_filesystem(item.collection_name)
|
|
156
|
+
|
|
157
|
+
dest_path = dest_dir / sanitize_for_filesystem(item.filename)
|
|
158
|
+
|
|
159
|
+
# Create individual file task
|
|
160
|
+
file_task = progress.add_task(
|
|
161
|
+
"download",
|
|
162
|
+
filename=item.filename,
|
|
163
|
+
start=False,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Perform download
|
|
167
|
+
if download_file(item, dest_path, progress, file_task):
|
|
168
|
+
success_count += 1
|
|
169
|
+
else:
|
|
170
|
+
failed_count += 1
|
|
171
|
+
|
|
172
|
+
# Update overall progress
|
|
173
|
+
progress.remove_task(file_task)
|
|
174
|
+
progress.advance(overall)
|
|
175
|
+
|
|
176
|
+
# Summary
|
|
177
|
+
console.print()
|
|
178
|
+
if failed_count == 0:
|
|
179
|
+
console.print(
|
|
180
|
+
f"[bold green]✓ Success![/bold green] Downloaded {success_count} files."
|
|
181
|
+
)
|
|
182
|
+
console.print(f"[dim]Location: {base_dir.absolute()}[/dim]")
|
|
183
|
+
else:
|
|
184
|
+
console.print(
|
|
185
|
+
f"[bold yellow]⚠ Completed with errors.[/bold yellow] "
|
|
186
|
+
f"{success_count} succeeded, {failed_count} failed."
|
|
187
|
+
)
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_plugin_name(url: str) -> str | None:
|
|
192
|
+
"""Get plugin name for UI feedback."""
|
|
193
|
+
from urllib.parse import urlparse
|
|
194
|
+
|
|
195
|
+
domain = urlparse(url).netloc
|
|
196
|
+
plugin_class = get_plugin_class(domain)
|
|
197
|
+
return plugin_class.__name__ if plugin_class else None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _print_json(url: str, items: list[mgl.DownloadItem]) -> None:
|
|
201
|
+
data = {
|
|
202
|
+
"source": url,
|
|
203
|
+
"count": len(items),
|
|
204
|
+
"items": [dataclasses.asdict(item) for item in items],
|
|
205
|
+
}
|
|
206
|
+
console.print_json(data=data)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _print_human_readable(items: list[mgl.DownloadItem]) -> None:
|
|
210
|
+
console.print(f"\n[bold]Found {len(items)} files:[/bold]\n")
|
|
211
|
+
|
|
212
|
+
for i, item in enumerate(items, 1):
|
|
213
|
+
console.print(f" [cyan]{i:02d}.[/cyan] {item.filename}")
|
|
214
|
+
|
|
215
|
+
if item.collection_name:
|
|
216
|
+
console.print(f" [dim]Collection: {item.collection_name}[/dim]")
|
|
217
|
+
|
|
218
|
+
if item.size_bytes:
|
|
219
|
+
size_mb = item.size_bytes / (1024 * 1024)
|
|
220
|
+
console.print(f" [dim]Size: {size_mb:.2f} MB[/dim]")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from megaloader.item import DownloadItem
|
|
6
|
+
from rich.progress import Progress, TaskID
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def download_file(
|
|
10
|
+
item: DownloadItem,
|
|
11
|
+
destination: Path,
|
|
12
|
+
progress: Progress,
|
|
13
|
+
task_id: TaskID,
|
|
14
|
+
) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Download a file with progress tracking.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
item: Download metadata including URL and required headers
|
|
20
|
+
destination: Full path where file will be saved
|
|
21
|
+
progress: Rich progress instance for UI updates
|
|
22
|
+
task_id: Task ID for this specific download
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
True if successful or skipped, False if failed
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
# Skip if file already exists
|
|
29
|
+
if destination.exists():
|
|
30
|
+
progress.console.print(
|
|
31
|
+
f"[yellow]⊙[/yellow] Skipped (exists): {item.filename}"
|
|
32
|
+
)
|
|
33
|
+
progress.advance(task_id, 100)
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
# Ensure parent directory exists
|
|
37
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
# Build headers
|
|
40
|
+
headers = {
|
|
41
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
|
42
|
+
**item.headers,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Stream download
|
|
46
|
+
with requests.get(
|
|
47
|
+
item.download_url,
|
|
48
|
+
stream=True,
|
|
49
|
+
timeout=60,
|
|
50
|
+
headers=headers,
|
|
51
|
+
) as response:
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
|
|
54
|
+
# Get file size for progress bar
|
|
55
|
+
total_size = int(response.headers.get("content-length", 0))
|
|
56
|
+
progress.update(task_id, total=total_size)
|
|
57
|
+
|
|
58
|
+
# Write to disk in chunks
|
|
59
|
+
with destination.open("wb") as f:
|
|
60
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
61
|
+
if chunk:
|
|
62
|
+
f.write(chunk)
|
|
63
|
+
progress.advance(task_id, len(chunk))
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
except (requests.RequestException, OSError) as e:
|
|
68
|
+
# Print error above progress bar
|
|
69
|
+
progress.console.print(f"[red]✗[/red] Failed: {item.filename} ({e!s})")
|
|
70
|
+
|
|
71
|
+
# Clean up partial download
|
|
72
|
+
if destination.exists():
|
|
73
|
+
destination.unlink(missing_ok=True)
|
|
74
|
+
|
|
75
|
+
return False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from megaloader.plugins import PLUGIN_REGISTRY
|
|
4
|
+
|
|
5
|
+
from megaloader_cli.commands import download_command, extract_command
|
|
6
|
+
from megaloader_cli.utils import console, setup_logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
@click.version_option(prog_name="megaloader")
|
|
11
|
+
def cli() -> None:
|
|
12
|
+
"""
|
|
13
|
+
Megaloader: Extract and download content from file hosting platforms.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
17
|
+
megaloader download https://gofile.io/d/xyz456 ./downloads
|
|
18
|
+
megaloader plugins
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command(name="extract")
|
|
23
|
+
@click.argument("url")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--json",
|
|
26
|
+
"output_json",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
help="Output JSON instead of human-readable text",
|
|
29
|
+
)
|
|
30
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable debug logging")
|
|
31
|
+
def extract_cmd(url: str, output_json: bool, verbose: bool) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Extract metadata from URL without downloading (dry run).
|
|
34
|
+
|
|
35
|
+
Shows what would be downloaded including filenames, sizes, and URLs.
|
|
36
|
+
"""
|
|
37
|
+
setup_logging(verbose)
|
|
38
|
+
extract_command(url, output_json)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@cli.command(name="download")
|
|
42
|
+
@click.argument("url")
|
|
43
|
+
@click.argument("output_dir", type=click.Path(), default="./downloads")
|
|
44
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable debug logging")
|
|
45
|
+
@click.option(
|
|
46
|
+
"--flat",
|
|
47
|
+
is_flag=True,
|
|
48
|
+
help="Save all files to output_dir (no collection subfolders)",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--filter",
|
|
52
|
+
"pattern",
|
|
53
|
+
help="Filter files by glob pattern (e.g., *.jpg, *.mp4)",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--password",
|
|
57
|
+
help="Password for protected content (Gofile)",
|
|
58
|
+
)
|
|
59
|
+
def download_cmd(
|
|
60
|
+
url: str,
|
|
61
|
+
output_dir: str,
|
|
62
|
+
verbose: bool,
|
|
63
|
+
flat: bool,
|
|
64
|
+
pattern: str | None,
|
|
65
|
+
password: str | None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Download content from URL to OUTPUT_DIR.
|
|
69
|
+
|
|
70
|
+
By default, files are organized into subfolders by collection.
|
|
71
|
+
Use --flat to disable this behavior.
|
|
72
|
+
"""
|
|
73
|
+
setup_logging(verbose)
|
|
74
|
+
|
|
75
|
+
options = {}
|
|
76
|
+
if password:
|
|
77
|
+
options["password"] = password
|
|
78
|
+
|
|
79
|
+
download_command(url, output_dir, flat, pattern, options)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cli.command(name="plugins")
|
|
83
|
+
def list_plugins_cmd() -> None:
|
|
84
|
+
"""List all supported websites and domains."""
|
|
85
|
+
console.print("\n[bold]Supported Platforms:[/bold]\n")
|
|
86
|
+
|
|
87
|
+
for domain in sorted(PLUGIN_REGISTRY.keys()):
|
|
88
|
+
plugin = PLUGIN_REGISTRY[domain]
|
|
89
|
+
console.print(f" • [cyan]{domain:<20}[/cyan] ({plugin.__name__})")
|
|
90
|
+
|
|
91
|
+
console.print()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
cli()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.logging import RichHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Shared console instance
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logging(verbose: bool) -> None:
|
|
13
|
+
level = logging.DEBUG if verbose else logging.WARNING
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=level,
|
|
17
|
+
format="%(message)s",
|
|
18
|
+
datefmt="[%X]",
|
|
19
|
+
handlers=[
|
|
20
|
+
RichHandler(
|
|
21
|
+
console=console,
|
|
22
|
+
rich_tracebacks=True,
|
|
23
|
+
markup=True,
|
|
24
|
+
)
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sanitize_for_filesystem(name: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Sanitize a string to be safe for use as a filename or directory name.
|
|
32
|
+
Removes/replaces characters that are invalid on Windows/Unix filesystems.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: Input string (e.g., "Video: Title?")
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Safe string (e.g., "Video_ Title_")
|
|
39
|
+
"""
|
|
40
|
+
# Replace invalid characters with underscore
|
|
41
|
+
sanitized = re.sub(r'[<>:"/\\|?*]', "_", name)
|
|
42
|
+
|
|
43
|
+
# Collapse multiple underscores
|
|
44
|
+
sanitized = re.sub(r"_+", "_", sanitized)
|
|
45
|
+
|
|
46
|
+
# Remove leading/trailing whitespace and underscores
|
|
47
|
+
sanitized = sanitized.strip().strip("_")
|
|
48
|
+
|
|
49
|
+
# Ensure not empty
|
|
50
|
+
return sanitized or "unnamed"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "megaloader-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line interface for Megaloader"
|
|
5
|
+
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "readme.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
|
|
10
|
+
maintainers = [
|
|
11
|
+
{ name = "David Duran", email = "dadch1404@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"megaloader",
|
|
16
|
+
"click>=8.1.0",
|
|
17
|
+
"rich>=13.7.0",
|
|
18
|
+
"requests>=2.32.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.uv.sources]
|
|
22
|
+
megaloader = { workspace = true }
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
megaloader = "megaloader_cli.main:cli"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["megaloader_cli"]
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
# click flags (--verbose, --force, etc.) are booleans by design
|
|
36
|
+
ignore = ["FBT001"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# [pkg]: megaloader-cli
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/megaloader-cli)
|
|
4
|
+
|
|
5
|
+
Command-line interface for the megaloader library. Extract metadata and download
|
|
6
|
+
files from supported hosting platforms directly in your terminal.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install megaloader-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The CLI installs the core megaloader library as a dependency. Both packages can
|
|
15
|
+
be used independently.
|
|
16
|
+
|
|
17
|
+
## Basic usage
|
|
18
|
+
|
|
19
|
+
List available files without downloading:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Download files to a directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
List supported platforms:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
megaloader plugins
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
The `extract` command shows file metadata without downloading anything. Add
|
|
40
|
+
`--json` for machine-readable output or `--verbose` for debug logs:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
44
|
+
megaloader extract https://gofile.io/d/xyz456 --json
|
|
45
|
+
megaloader extract https://cyberdrop.me/a/album --verbose
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `download` command saves files to the specified directory. Defaults to
|
|
49
|
+
`./downloads` when omitted. Files are grouped into collection subfolders by
|
|
50
|
+
default:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
megaloader download https://pixeldrain.com/l/abc123
|
|
54
|
+
megaloader download https://cyberdrop.me/a/album ./my-files
|
|
55
|
+
megaloader download https://bunkr.si/a/xyz789 ./downloads --verbose
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Add `--flat` to disable subfolders and write all files directly to the output
|
|
59
|
+
directory. Use `--filter` to download only files matching a glob pattern:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads --flat
|
|
63
|
+
megaloader download https://cyberdrop.me/a/album ./videos --filter "*.mp4"
|
|
64
|
+
megaloader download https://bunkr.si/a/xyz789 ./images --filter "*.{jpg,png}"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
GoFile password-protected content requires the `--password` argument:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
megaloader download https://gofile.io/d/protected ./downloads --password secret123
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `plugins` command lists all supported platforms and domains.
|
|
74
|
+
|
|
75
|
+
## Common patterns
|
|
76
|
+
|
|
77
|
+
Preview files before downloading:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
megaloader extract https://pixeldrain.com/l/abc123
|
|
81
|
+
megaloader download https://pixeldrain.com/l/abc123 ./downloads
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Filter by type to download only what you need:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
megaloader download https://cyberdrop.me/a/album ./videos --filter "*.mp4"
|
|
88
|
+
megaloader download https://bunkr.si/a/xyz ./images --filter "*.jpg"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Control file organization with the `--flat` flag:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
megaloader download https://pixeldrain.com/l/abc ./organized
|
|
95
|
+
megaloader download https://pixeldrain.com/l/abc ./flat --flat
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Enable verbose logging for troubleshooting:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
megaloader download https://example.com/file ./downloads --verbose
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Relationship to core library
|
|
105
|
+
|
|
106
|
+
The CLI wraps the core megaloader library. It handles argument parsing, output
|
|
107
|
+
formatting, and file downloads. The core library performs URL detection,
|
|
108
|
+
metadata extraction, and platform-specific logic.
|
|
109
|
+
|
|
110
|
+
Use the CLI for terminal workflows, quick downloads, and platform exploration.
|
|
111
|
+
Use the core library for Python integration, custom handling, progress tracking,
|
|
112
|
+
and batch processing. See the core library documentation for programmatic usage.
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
The CLI is part of a uv workspace. Install from the repository root:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv sync
|
|
120
|
+
uv run megaloader --help
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Install in editable mode for development:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv pip install -e packages/cli
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run `uv run ruff format .` and the test suite before committing.
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Contributions are welcome. See the repository contributing guide for setup and
|
|
134
|
+
submission details. Report bugs and request features through GitHub Discussions.
|
|
135
|
+
Include your Python version, error messages, and problematic URLs.
|