megaloader-cli 0.1.0__py3-none-any.whl

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.
@@ -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]")
megaloader_cli/io.py ADDED
@@ -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
megaloader_cli/main.py ADDED
@@ -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,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
+ [![PyPI version](https://badge.fury.io/py/megaloader-cli.svg)](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,9 @@
1
+ megaloader_cli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ megaloader_cli/commands.py,sha256=OaiktrZbkwsdRID0EM7ymphAH_vVr2BvkMD0I3PEOFc,6478
3
+ megaloader_cli/io.py,sha256=8WvvVbzihM6T5aJd0f7APWF_mlhkWDszoB422oZ9zQM,2174
4
+ megaloader_cli/main.py,sha256=SZfiLOBLDzRrifty6hY1B-_IQfjk9R5on-ggbbbFq9c,2464
5
+ megaloader_cli/utils.py,sha256=yQEZYqLDv-xGTpm3v73SolD2izXtvNF7ygcx3myhj5g,1203
6
+ megaloader_cli-0.1.0.dist-info/METADATA,sha256=c9pAsUCKo0hdNDnxHjhXqf3L7DgzWIXCAV7YMLOn6jU,4115
7
+ megaloader_cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ megaloader_cli-0.1.0.dist-info/entry_points.txt,sha256=xjDAeezSoZ1KOUmxQh-mImSGbaLIJSyiXOLokVY7dug,55
9
+ megaloader_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ megaloader = megaloader_cli.main:cli