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.
@@ -0,0 +1,363 @@
1
+ import typer
2
+ import json
3
+ from pathlib import Path
4
+ from rich.prompt import Prompt, Confirm
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+ from rich import box
8
+
9
+ from max_cli.common.logger import console, log_success, log_error
10
+ from max_cli.config import settings
11
+
12
+ app = typer.Typer()
13
+
14
+ GLOBAL_CONFIG_PATH = Path.home() / ".max_config.env"
15
+ LOCAL_CONFIG_PATH = Path("source.env")
16
+
17
+
18
+ @app.command("setup")
19
+ def setup_config():
20
+ """
21
+ Interactive wizard to configure Global Settings (API Keys, Models, URLs).
22
+ """
23
+ console.print(
24
+ Panel(
25
+ "[bold cyan]Max CLI Configuration Wizard[/bold cyan]", border_style="cyan"
26
+ )
27
+ )
28
+ console.print(f"Settings will be saved to: [dim]{GLOBAL_CONFIG_PATH}[/dim]\n")
29
+
30
+ config_data = {}
31
+
32
+ # --- 1. Provider Selection ---
33
+ provider = Prompt.ask(
34
+ "Select your AI Provider",
35
+ choices=["gemini", "openai", "custom"],
36
+ default="gemini",
37
+ )
38
+
39
+ if provider == "gemini":
40
+ config_data["OPENAI_BASE_URL"] = (
41
+ "https://generativelanguage.googleapis.com/v1beta/openai/"
42
+ )
43
+ default_text_model = "gemini-1.5-flash"
44
+ default_img_model = "gemini-2.5-flash-image"
45
+ elif provider == "openai":
46
+ config_data["OPENAI_BASE_URL"] = "" # OpenAI uses default
47
+ default_text_model = "gpt-4o"
48
+ default_img_model = "dall-e-3"
49
+ else:
50
+ # Custom Provider (LocalAI, Ollama, etc.)
51
+ config_data["OPENAI_BASE_URL"] = Prompt.ask("Enter Custom Base URL")
52
+ default_text_model = "gpt-3.5-turbo"
53
+ default_img_model = "dall-e-3"
54
+
55
+ # --- 2. API Key ---
56
+ api_key = Prompt.ask(f"Enter {provider.capitalize()} API Key", password=True)
57
+ config_data["OPENAI_API_KEY"] = api_key
58
+
59
+ # --- 3. Model Configuration (Override Defaults) ---
60
+ console.print("\n[bold]Model Configuration[/bold] (Press Enter to keep default)")
61
+
62
+ config_data["AI_MODEL"] = Prompt.ask("Text/Logic Model", default=default_text_model)
63
+
64
+ config_data["AI_IMAGE_MODEL"] = Prompt.ask(
65
+ "Image Generation Model", default=default_img_model
66
+ )
67
+
68
+ # --- 4. Write to Global File ---
69
+ try:
70
+ _write_env_file(GLOBAL_CONFIG_PATH, config_data)
71
+ log_success("Configuration updated successfully!")
72
+ console.print(f"[green]Global settings saved to {GLOBAL_CONFIG_PATH}[/green]")
73
+ except Exception as e:
74
+ log_error(f"Failed to save config: {e}")
75
+
76
+
77
+ @app.command("save")
78
+ def save_local_to_global(
79
+ force: bool = typer.Option(
80
+ False, "--force", "-f", help="Overwrite global config without asking."
81
+ ),
82
+ ):
83
+ """
84
+ Promote the current folder's .env file to Global Settings.
85
+ Useful if you configured a local project perfectly and want to make it the system default.
86
+ """
87
+ local_env = Path(".env")
88
+
89
+ if not local_env.exists():
90
+ log_error("No .env file found in the current directory.")
91
+ console.print(
92
+ "Run [bold]max config setup[/bold] to create a new configuration."
93
+ )
94
+ raise typer.Exit(1)
95
+
96
+ console.print(f"Found local config at: [bold]{local_env.resolve()}[/bold]")
97
+
98
+ # Read local content to preview (optional security check)
99
+ content = local_env.read_text()
100
+
101
+ if GLOBAL_CONFIG_PATH.exists() and not force:
102
+ console.print(
103
+ f"[yellow]Warning: This will overwrite your global settings at {GLOBAL_CONFIG_PATH}[/yellow]"
104
+ )
105
+ if not Confirm.ask("Are you sure?"):
106
+ console.print("[red]Aborted.[/red]")
107
+ raise typer.Exit(1)
108
+
109
+ try:
110
+ # We simply copy the file content
111
+ GLOBAL_CONFIG_PATH.write_text(content)
112
+ log_success("Local .env saved as Global Configuration!")
113
+ console.print(f"[dim]Copied to: {GLOBAL_CONFIG_PATH}[/dim]")
114
+ except Exception as e:
115
+ log_error(f"Failed to copy file: {e}")
116
+
117
+
118
+ @app.command("show")
119
+ def show_config():
120
+ """Display where Max is loading settings from."""
121
+
122
+ # Check Global
123
+ if GLOBAL_CONFIG_PATH.exists():
124
+ console.print(
125
+ f"🌍 [bold green]Global Config Found:[/bold green] {GLOBAL_CONFIG_PATH}"
126
+ )
127
+ else:
128
+ console.print(
129
+ "🌍 [bold red]Global Config Missing[/bold red] (Run 'max config setup')"
130
+ )
131
+
132
+ # Check Local
133
+ local_env = Path(".env")
134
+ if local_env.exists():
135
+ console.print(
136
+ f"📂 [bold cyan]Local Override Found:[/bold cyan] {local_env.resolve()}"
137
+ )
138
+ console.print("[dim]Local settings take priority over Global settings.[/dim]")
139
+
140
+ # Show Active Models
141
+ console.print("\n[bold]Active Configuration:[/bold]")
142
+ console.print(f"Text Model: [green]{settings.AI_MODEL}[/green]")
143
+ console.print(f"Image Model: [green]{settings.AI_IMAGE_MODEL}[/green]")
144
+ if settings.OPENAI_BASE_URL:
145
+ console.print(f"Base URL: [dim]{settings.OPENAI_BASE_URL}[/dim]")
146
+
147
+
148
+ @app.command("grab")
149
+ def configure_grab():
150
+ """
151
+ Configure default settings for the Media Downloader.
152
+ """
153
+ console.print(
154
+ Panel("[bold cyan]Downloader Preferences[/bold cyan]", border_style="cyan")
155
+ )
156
+
157
+ current_data = {}
158
+
159
+ # 1. Default Quality
160
+ q_choice = Prompt.ask(
161
+ "Default Video/Audio Quality?",
162
+ choices=["s", "m", "h", "x"],
163
+ default=settings.GRAB_QUALITY,
164
+ )
165
+ current_data["GRAB_QUALITY"] = q_choice
166
+
167
+ # 2. Playlist Behavior
168
+ strip_pl = Confirm.ask(
169
+ "Auto-strip Playlist info?", default=settings.GRAB_STRIP_PLAYLIST
170
+ )
171
+ console.print("[dim] (If Yes: 'watch?v=ID&list=LIST' becomes 'watch?v=ID')[/dim]")
172
+ current_data["GRAB_STRIP_PLAYLIST"] = str(strip_pl)
173
+
174
+ # 3. Metadata
175
+ meta = Confirm.ask(
176
+ "Embed Metadata (Tags/Thumbnail)?", default=settings.GRAB_INCLUDE_METADATA
177
+ )
178
+ current_data["GRAB_INCLUDE_METADATA"] = str(meta)
179
+
180
+ # 4. Save
181
+ try:
182
+ # We append/update the global config file
183
+ lines = []
184
+ if GLOBAL_CONFIG_PATH.exists():
185
+ lines = GLOBAL_CONFIG_PATH.read_text().splitlines()
186
+
187
+ # Remove old entries for these specific keys to avoid duplicates
188
+ keys = ["GRAB_QUALITY", "GRAB_STRIP_PLAYLIST", "GRAB_INCLUDE_METADATA"]
189
+ lines = [line for line in lines if not any(line.startswith(k) for k in keys)]
190
+
191
+ # Add new
192
+ for k, v in current_data.items():
193
+ lines.append(f"{k}={v}")
194
+
195
+ GLOBAL_CONFIG_PATH.write_text("\n".join(lines) + "\n")
196
+ log_success("Downloader settings saved!")
197
+
198
+ except Exception as e:
199
+ log_error(f"Failed to save settings: {e}")
200
+
201
+
202
+ def _write_env_file(path: Path, data: dict):
203
+ """Helper to write a clean .env file."""
204
+ lines = [
205
+ "# Max CLI Global Configuration",
206
+ "# Created automatically via 'max config setup'",
207
+ "",
208
+ ]
209
+ for key, value in data.items():
210
+ if value is not None:
211
+ lines.append(f"{key}={value}")
212
+
213
+ path.write_text("\n".join(lines) + "\n")
214
+
215
+
216
+ @app.command("reset")
217
+ def reset_config(
218
+ global_only: bool = typer.Option(
219
+ False, "--global", help="Only reset global config."
220
+ ),
221
+ local_only: bool = typer.Option(False, "--local", help="Only reset local .env."),
222
+ ):
223
+ """Reset configuration to defaults."""
224
+ if not global_only and not local_only:
225
+ global_only = True
226
+ local_only = True
227
+
228
+ if global_only and GLOBAL_CONFIG_PATH.exists():
229
+ if Confirm.ask(f"Delete global config at {GLOBAL_CONFIG_PATH}?"):
230
+ GLOBAL_CONFIG_PATH.unlink()
231
+ log_success("Global config reset to defaults.")
232
+
233
+ if local_only:
234
+ local_env = Path(".env")
235
+ if local_env.exists():
236
+ if Confirm.ask(f"Delete local config at {local_env}?"):
237
+ local_env.unlink()
238
+ log_success("Local config reset to defaults.")
239
+
240
+
241
+ @app.command("validate")
242
+ def validate_config():
243
+ """Validate current configuration."""
244
+ table = Table(title="Configuration Validation", box=box.ROUNDED)
245
+ table.add_column("Setting", style="cyan")
246
+ table.add_column("Value")
247
+ table.add_column("Status", justify="center")
248
+
249
+ issues = []
250
+
251
+ table.add_row("MAX_WORKERS", str(settings.MAX_WORKERS), "[green]OK[/green]")
252
+ table.add_row("BATCH_SIZE", str(settings.BATCH_SIZE), "[green]OK[/green]")
253
+ table.add_row(
254
+ "DOWNLOAD_TIMEOUT", str(settings.DOWNLOAD_TIMEOUT), "[green]OK[/green]"
255
+ )
256
+ table.add_row("DEFAULT_QUALITY", str(settings.DEFAULT_QUALITY), "[green]OK[/green]")
257
+
258
+ if settings.MAX_WORKERS < 1 or settings.MAX_WORKERS > 16:
259
+ table.add_row("MAX_WORKERS", str(settings.MAX_WORKERS), "[red]Invalid[/red]")
260
+ issues.append("MAX_WORKERS must be between 1 and 16")
261
+ else:
262
+ table.add_row("MAX_WORKERS", str(settings.MAX_WORKERS), "[green]OK[/green]")
263
+
264
+ if settings.DEFAULT_QUALITY < 1 or settings.DEFAULT_QUALITY > 100:
265
+ issues.append("DEFAULT_QUALITY must be between 1 and 100")
266
+
267
+ if settings.DOWNLOAD_TIMEOUT < 30:
268
+ issues.append("DOWNLOAD_TIMEOUT must be at least 30 seconds")
269
+
270
+ if settings.OPENAI_API_KEY:
271
+ table.add_row("OPENAI_API_KEY", "***configured***", "[green]OK[/green]")
272
+ else:
273
+ table.add_row("OPENAI_API_KEY", "[not set]", "[yellow]Warning[/yellow]")
274
+
275
+ console.print(table)
276
+
277
+ if issues:
278
+ console.print("[bold red]Issues found:[/bold red]")
279
+ for issue in issues:
280
+ console.print(f" [red]•[/red] {issue}")
281
+ else:
282
+ log_success("Configuration is valid!")
283
+
284
+
285
+ @app.command("export")
286
+ def export_config(
287
+ output: Path = typer.Option(Path("max-config.json"), "-o", help="Output file."),
288
+ include_defaults: bool = typer.Option(
289
+ False, "--include-defaults", help="Include default values."
290
+ ),
291
+ ):
292
+ """Export configuration to JSON file."""
293
+ config_dict = {}
294
+
295
+ if include_defaults:
296
+ config_dict = {
297
+ "APP_NAME": settings.APP_NAME,
298
+ "DEFAULT_QUALITY": settings.DEFAULT_QUALITY,
299
+ "MAX_WORKERS": settings.MAX_WORKERS,
300
+ "BATCH_SIZE": settings.BATCH_SIZE,
301
+ "DOWNLOAD_TIMEOUT": settings.DOWNLOAD_TIMEOUT,
302
+ "MAX_RETRIES": settings.MAX_RETRIES,
303
+ "PROGRESS_BAR": settings.PROGRESS_BAR,
304
+ "VERBOSE": settings.VERBOSE,
305
+ "CONFIRM_DESTRUCTIVE": settings.CONFIRM_DESTRUCTIVE,
306
+ "AI_MODEL": settings.AI_MODEL,
307
+ "AI_IMAGE_MODEL": settings.AI_IMAGE_MODEL,
308
+ "GRAB_QUALITY": settings.GRAB_QUALITY,
309
+ "GRAB_AUDIO_FORMAT": settings.GRAB_AUDIO_FORMAT,
310
+ "GRAB_STRIP_PLAYLIST": settings.GRAB_STRIP_PLAYLIST,
311
+ "GRAB_INCLUDE_METADATA": settings.GRAB_INCLUDE_METADATA,
312
+ }
313
+ else:
314
+ non_defaults = {
315
+ "OPENAI_API_KEY": settings.OPENAI_API_KEY,
316
+ "OPENAI_BASE_URL": settings.OPENAI_BASE_URL,
317
+ "AI_MODEL": settings.AI_MODEL,
318
+ "AI_IMAGE_MODEL": settings.AI_IMAGE_MODEL,
319
+ "GRAB_QUALITY": settings.GRAB_QUALITY,
320
+ "GRAB_AUDIO_FORMAT": settings.GRAB_AUDIO_FORMAT,
321
+ "GRAB_STRIP_PLAYLIST": settings.GRAB_STRIP_PLAYLIST,
322
+ "GRAB_INCLUDE_METADATA": settings.GRAB_INCLUDE_METADATA,
323
+ }
324
+ for k, v in non_defaults.items():
325
+ if v is not None and v != "":
326
+ config_dict[k] = v
327
+
328
+ try:
329
+ output.write_text(json.dumps(config_dict, indent=2))
330
+ log_success(f"Config exported to {output}")
331
+ except Exception as e:
332
+ log_error(f"Failed to export config: {e}")
333
+
334
+
335
+ @app.command("import")
336
+ def import_config(
337
+ input: Path = typer.Argument(..., help="Input JSON file."),
338
+ global_config: bool = typer.Option(
339
+ True, "--global/--local", help="Import to global or local config."
340
+ ),
341
+ ):
342
+ """Import configuration from JSON file."""
343
+ if not input.exists():
344
+ log_error(f"File not found: {input}")
345
+ raise typer.Exit(1)
346
+
347
+ try:
348
+ data = json.loads(input.read_text())
349
+ except Exception as e:
350
+ log_error(f"Invalid JSON: {e}")
351
+ raise typer.Exit(1)
352
+
353
+ target = GLOBAL_CONFIG_PATH if global_config else Path(".env")
354
+
355
+ if target.exists() and not Confirm.ask(f"Overwrite {target}?"):
356
+ console.print("[red]Aborted.[/red]")
357
+ raise typer.Exit(1)
358
+
359
+ try:
360
+ _write_env_file(target, data)
361
+ log_success(f"Config imported to {target}")
362
+ except Exception as e:
363
+ log_error(f"Failed to import config: {e}")