max-cli 0.2.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.
- max_cli/__init__.py +0 -0
- max_cli/common/cache.py +145 -0
- max_cli/common/concurrent.py +83 -0
- max_cli/common/exceptions.py +40 -0
- max_cli/common/logger.py +22 -0
- max_cli/common/logging.py +24 -0
- max_cli/common/retry.py +51 -0
- max_cli/common/utils.py +40 -0
- max_cli/config.py +43 -0
- max_cli/core/ai_engine.py +541 -0
- max_cli/core/file_organizer.py +254 -0
- max_cli/core/image_processor.py +139 -0
- max_cli/core/media_engine.py +681 -0
- max_cli/core/network_engine.py +103 -0
- max_cli/core/pdf_engine.py +520 -0
- max_cli/core/system_engine.py +57 -0
- max_cli/interface/cli_ai.py +376 -0
- max_cli/interface/cli_config.py +363 -0
- max_cli/interface/cli_files.py +388 -0
- max_cli/interface/cli_images.py +176 -0
- max_cli/interface/cli_media.py +558 -0
- max_cli/interface/cli_network.py +174 -0
- max_cli/interface/cli_pdf.py +651 -0
- max_cli/interface/cli_tools.py +60 -0
- max_cli/main.py +91 -0
- max_cli/plugins/__init__.py +4 -0
- max_cli/plugins/base.py +39 -0
- max_cli/plugins/manager.py +81 -0
- max_cli-0.2.0.dist-info/METADATA +632 -0
- max_cli-0.2.0.dist-info/RECORD +34 -0
- max_cli-0.2.0.dist-info/WHEEL +5 -0
- max_cli-0.2.0.dist-info/entry_points.txt +2 -0
- max_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- max_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from rich.progress import (
|
|
6
|
+
Progress,
|
|
7
|
+
SpinnerColumn,
|
|
8
|
+
BarColumn,
|
|
9
|
+
TextColumn,
|
|
10
|
+
DownloadColumn,
|
|
11
|
+
TransferSpeedColumn,
|
|
12
|
+
TimeRemainingColumn,
|
|
13
|
+
)
|
|
14
|
+
from rich.prompt import Confirm, Prompt
|
|
15
|
+
|
|
16
|
+
from max_cli.core.network_engine import NetworkEngine
|
|
17
|
+
from max_cli.common.logger import console, log_success, log_error
|
|
18
|
+
from max_cli.config import settings
|
|
19
|
+
|
|
20
|
+
app = typer.Typer()
|
|
21
|
+
engine = NetworkEngine()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _clean_url(url: str, strip_playlist: bool) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Removes playlist params (list, index) if the URL points to a specific video (v=...).
|
|
27
|
+
"""
|
|
28
|
+
if not strip_playlist:
|
|
29
|
+
return url
|
|
30
|
+
|
|
31
|
+
parsed = urllib.parse.urlparse(url)
|
|
32
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
33
|
+
|
|
34
|
+
# Only strip if it's a video AND a list (e.g. youtube.com/watch?v=...&list=...)
|
|
35
|
+
if "v" in query and "list" in query:
|
|
36
|
+
# Rebuild query with only 'v' (and keep other stuff like 't' for timestamp)
|
|
37
|
+
new_query = {"v": query["v"]}
|
|
38
|
+
if "t" in query:
|
|
39
|
+
new_query["t"] = query["t"]
|
|
40
|
+
|
|
41
|
+
new_parts = list(parsed)
|
|
42
|
+
new_parts[4] = urllib.parse.urlencode(new_query, doseq=True)
|
|
43
|
+
cleaned_url = urllib.parse.urlunparse(new_parts)
|
|
44
|
+
|
|
45
|
+
console.print("[dim]Auto-cleaned URL: Removed playlist info.[/dim]")
|
|
46
|
+
return cleaned_url
|
|
47
|
+
|
|
48
|
+
return url
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("grab")
|
|
52
|
+
def download_media(
|
|
53
|
+
url: str = typer.Argument(..., help="URL to download."),
|
|
54
|
+
# We use Optional[str] = None so we can detect if the User provided a flag or not.
|
|
55
|
+
quality: Optional[str] = typer.Option(
|
|
56
|
+
None,
|
|
57
|
+
"--quality",
|
|
58
|
+
"-q",
|
|
59
|
+
help=f"Quality: [s]mall, [m]edium, [h]igh, [x]best. (Default: {settings.GRAB_QUALITY})",
|
|
60
|
+
),
|
|
61
|
+
audio: bool = typer.Option(False, "--audio", "-a", help="Audio only."),
|
|
62
|
+
# Index/Playlist Controls
|
|
63
|
+
index: str = typer.Option(
|
|
64
|
+
None, "--index", "-i", help="Playlist index (e.g. '1', '1-5')."
|
|
65
|
+
),
|
|
66
|
+
no_playlist: bool = typer.Option(
|
|
67
|
+
False, "--no-playlist", help="Force single video download."
|
|
68
|
+
),
|
|
69
|
+
# Metadata: We default to None to allow Config fallback, but allow --no-meta/--nom to override
|
|
70
|
+
no_meta: bool = typer.Option(
|
|
71
|
+
False,
|
|
72
|
+
"--no-meta",
|
|
73
|
+
"--nom", # <--- New Shortcut
|
|
74
|
+
help=f"Disable metadata/thumbnails. (Default Setting: {'Include' if settings.GRAB_INCLUDE_METADATA else 'Exclude'})",
|
|
75
|
+
),
|
|
76
|
+
output: Path = typer.Option(Path("."), "--output", "-o"),
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Download media using saved preferences or overrides.
|
|
80
|
+
"""
|
|
81
|
+
if not output.exists():
|
|
82
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
# --- 1. Resolve Settings Priority (Flag > Config > Default) ---
|
|
85
|
+
final_quality = quality if quality else settings.GRAB_QUALITY
|
|
86
|
+
|
|
87
|
+
# Logic for Metadata:
|
|
88
|
+
# If user passed --no-meta (True), we force False.
|
|
89
|
+
# If user didn't pass it (False), we use the Config setting.
|
|
90
|
+
include_metadata = False if no_meta else settings.GRAB_INCLUDE_METADATA
|
|
91
|
+
|
|
92
|
+
# --- 2. URL Cleaning ---
|
|
93
|
+
# If the user explicitly asks for an index or no_playlist, we don't mess with the URL.
|
|
94
|
+
# Otherwise, we check the config preference.
|
|
95
|
+
if not index and not no_playlist:
|
|
96
|
+
url = _clean_url(url, settings.GRAB_STRIP_PLAYLIST)
|
|
97
|
+
|
|
98
|
+
# --- 3. Playlist Check Logic ---
|
|
99
|
+
# Only perform the check if we haven't already stripped the playlist info
|
|
100
|
+
should_check_playlist = ("list=" in url) and (not no_playlist) and (not index)
|
|
101
|
+
|
|
102
|
+
if should_check_playlist:
|
|
103
|
+
with console.status("[dim]Checking URL...[/dim]"):
|
|
104
|
+
try:
|
|
105
|
+
info = engine.get_info(url)
|
|
106
|
+
if "entries" in info:
|
|
107
|
+
count = len(info["entries"])
|
|
108
|
+
if not Confirm.ask(
|
|
109
|
+
f"[yellow]Playlist detected ({count} items). Download ALL?[/yellow]"
|
|
110
|
+
):
|
|
111
|
+
choice = Prompt.ask(
|
|
112
|
+
"Enter [bold]index[/bold] (e.g. 1) or [bold]n[/bold] to cancel",
|
|
113
|
+
default="n",
|
|
114
|
+
)
|
|
115
|
+
if choice.lower() == "n":
|
|
116
|
+
raise typer.Exit()
|
|
117
|
+
index = choice
|
|
118
|
+
except Exception:
|
|
119
|
+
pass # Fail silently on check, let downloader handle errors
|
|
120
|
+
|
|
121
|
+
# --- 4. Execution ---
|
|
122
|
+
console.print(
|
|
123
|
+
f"[cyan]Grabbing {'Audio' if audio else 'Video'} ({final_quality.upper()})...[/cyan]"
|
|
124
|
+
)
|
|
125
|
+
if not include_metadata:
|
|
126
|
+
console.print("[dim]Metadata disabled.[/dim]")
|
|
127
|
+
|
|
128
|
+
# Setup Rich Progress (Same as before)
|
|
129
|
+
progress = Progress(
|
|
130
|
+
SpinnerColumn(),
|
|
131
|
+
TextColumn("[bold blue]{task.fields[filename]}", justify="left"),
|
|
132
|
+
BarColumn(bar_width=None),
|
|
133
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
134
|
+
"•",
|
|
135
|
+
DownloadColumn(),
|
|
136
|
+
"•",
|
|
137
|
+
TransferSpeedColumn(),
|
|
138
|
+
"•",
|
|
139
|
+
TimeRemainingColumn(),
|
|
140
|
+
console=console,
|
|
141
|
+
transient=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
task_id = progress.add_task("Starting...", filename="Fetching info...", start=False)
|
|
145
|
+
|
|
146
|
+
def rich_hook(d):
|
|
147
|
+
if d["status"] == "downloading":
|
|
148
|
+
filename = d.get("filename", "").split("/")[-1]
|
|
149
|
+
filename = (filename[:30] + "...") if len(filename) > 30 else filename
|
|
150
|
+
progress.update(
|
|
151
|
+
task_id,
|
|
152
|
+
total=d.get("total_bytes") or d.get("total_bytes_estimate"),
|
|
153
|
+
completed=d.get("downloaded_bytes"),
|
|
154
|
+
filename=filename,
|
|
155
|
+
start=True,
|
|
156
|
+
)
|
|
157
|
+
elif d["status"] == "finished":
|
|
158
|
+
progress.update(task_id, filename="Processing...")
|
|
159
|
+
|
|
160
|
+
with progress:
|
|
161
|
+
try:
|
|
162
|
+
engine.download_media(
|
|
163
|
+
url=url,
|
|
164
|
+
output_path=output,
|
|
165
|
+
quality=final_quality, # Use the resolved quality
|
|
166
|
+
audio_only=audio,
|
|
167
|
+
include_metadata=include_metadata, # Use resolved meta
|
|
168
|
+
playlist_items=index,
|
|
169
|
+
no_playlist=no_playlist,
|
|
170
|
+
progress_hook=rich_hook,
|
|
171
|
+
)
|
|
172
|
+
log_success("Download Finished.")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
log_error(str(e))
|