vidsmith 1.0.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.
- vidsmith/__init__.py +3 -0
- vidsmith/cli/__init__.py +0 -0
- vidsmith/cli/app.py +184 -0
- vidsmith/cli/commands/__init__.py +0 -0
- vidsmith/cli/doctor.py +218 -0
- vidsmith/cli/executor.py +1264 -0
- vidsmith/cli/menus.py +186 -0
- vidsmith/cli/output.py +61 -0
- vidsmith/cli/summary/__init__.py +5 -0
- vidsmith/cli/summary/builder.py +298 -0
- vidsmith/cli/summary/model.py +24 -0
- vidsmith/cli/summary/renderer.py +74 -0
- vidsmith/cli/wizard/__init__.py +33 -0
- vidsmith/cli/wizard/base.py +160 -0
- vidsmith/cli/wizard/chrome.py +138 -0
- vidsmith/cli/wizard/dispatcher.py +176 -0
- vidsmith/cli/wizard/registry.py +70 -0
- vidsmith/cli/wizard/steps/__init__.py +16 -0
- vidsmith/cli/wizard/steps/_base.py +39 -0
- vidsmith/cli/wizard/steps/choice.py +119 -0
- vidsmith/cli/wizard/steps/confirm.py +83 -0
- vidsmith/cli/wizard/steps/multi_select.py +114 -0
- vidsmith/cli/wizard/steps/numeric.py +86 -0
- vidsmith/cli/wizard/steps/text_input.py +86 -0
- vidsmith/cli/wizard/steps/toggle.py +71 -0
- vidsmith/cli/wizard/wizards/__init__.py +13 -0
- vidsmith/cli/wizard/wizards/audio.py +128 -0
- vidsmith/cli/wizard/wizards/playlist.py +104 -0
- vidsmith/cli/wizard/wizards/settings.py +197 -0
- vidsmith/cli/wizard/wizards/subtitles.py +81 -0
- vidsmith/cli/wizard/wizards/transcript.py +104 -0
- vidsmith/cli/wizard/wizards/video.py +276 -0
- vidsmith/config/__init__.py +6 -0
- vidsmith/downloader/__init__.py +31 -0
- vidsmith/downloader/cleanup.py +172 -0
- vidsmith/downloader/engine.py +105 -0
- vidsmith/downloader/job.py +151 -0
- vidsmith/downloader/manager.py +124 -0
- vidsmith/downloader/progress.py +77 -0
- vidsmith/downloader/queue.py +96 -0
- vidsmith/downloader/validator.py +58 -0
- vidsmith/downloader/validators/__init__.py +20 -0
- vidsmith/downloader/validators/audio.py +57 -0
- vidsmith/downloader/validators/context.py +96 -0
- vidsmith/downloader/validators/file.py +24 -0
- vidsmith/downloader/validators/metadata.py +33 -0
- vidsmith/downloader/validators/models.py +77 -0
- vidsmith/downloader/validators/subtitle.py +101 -0
- vidsmith/downloader/validators/thumbnail.py +104 -0
- vidsmith/main.py +86 -0
- vidsmith/metadata/__init__.py +0 -0
- vidsmith/metadata/analyzer.py +220 -0
- vidsmith/models/__init__.py +3 -0
- vidsmith/models/media.py +66 -0
- vidsmith/playlist/__init__.py +48 -0
- vidsmith/playlist/batch.py +142 -0
- vidsmith/playlist/engine.py +163 -0
- vidsmith/playlist/exceptions.py +25 -0
- vidsmith/playlist/models.py +206 -0
- vidsmith/playlist/queue.py +186 -0
- vidsmith/processing/__init__.py +37 -0
- vidsmith/processing/exceptions.py +29 -0
- vidsmith/processing/ffmpeg.py +314 -0
- vidsmith/processing/models.py +98 -0
- vidsmith/processing/processor.py +23 -0
- vidsmith/providers/__init__.py +40 -0
- vidsmith/providers/base.py +99 -0
- vidsmith/providers/capabilities.py +72 -0
- vidsmith/providers/metadata.py +80 -0
- vidsmith/providers/results.py +35 -0
- vidsmith/providers/youtube.py +2018 -0
- vidsmith/settings/__init__.py +35 -0
- vidsmith/settings/store.py +193 -0
- vidsmith/subtitle/__init__.py +151 -0
- vidsmith/thumbnail/__init__.py +0 -0
- vidsmith/transcript/__init__.py +44 -0
- vidsmith/transcript/cleaner.py +97 -0
- vidsmith/transcript/engine.py +133 -0
- vidsmith/transcript/exceptions.py +21 -0
- vidsmith/transcript/json_export.py +31 -0
- vidsmith/transcript/markdown.py +27 -0
- vidsmith/transcript/models.py +87 -0
- vidsmith/transcript/parser.py +178 -0
- vidsmith/transcript/text.py +35 -0
- vidsmith/utils/__init__.py +0 -0
- vidsmith/utils/console.py +22 -0
- vidsmith/utils/environment.py +157 -0
- vidsmith/utils/exceptions.py +26 -0
- vidsmith/utils/logging.py +45 -0
- vidsmith/utils/validators.py +22 -0
- vidsmith-1.0.0.dist-info/METADATA +328 -0
- vidsmith-1.0.0.dist-info/RECORD +95 -0
- vidsmith-1.0.0.dist-info/WHEEL +4 -0
- vidsmith-1.0.0.dist-info/entry_points.txt +2 -0
- vidsmith-1.0.0.dist-info/licenses/LICENSE +21 -0
vidsmith/__init__.py
ADDED
vidsmith/cli/__init__.py
ADDED
|
File without changes
|
vidsmith/cli/app.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive application loop.
|
|
3
|
+
Entry: run()
|
|
4
|
+
Flow: banner → URL prompt → analyse (spinner) → type-dispatch → menu → wizard loop
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
from rich.status import Status
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from vidsmith.cli.menus import (
|
|
15
|
+
ACTION_BACK,
|
|
16
|
+
show_playlist_menu,
|
|
17
|
+
show_video_menu,
|
|
18
|
+
)
|
|
19
|
+
from vidsmith.cli.output import print_banner, print_rule
|
|
20
|
+
from vidsmith.cli.wizard.dispatcher import dispatch_wizard
|
|
21
|
+
from vidsmith.metadata.analyzer import analyze
|
|
22
|
+
from vidsmith.models.media import AnalysisResult, MediaType
|
|
23
|
+
from vidsmith.utils.console import console
|
|
24
|
+
from vidsmith.utils.exceptions import AnalysisError, UnsupportedURLError
|
|
25
|
+
from vidsmith.utils.validators import is_youtube_url
|
|
26
|
+
|
|
27
|
+
# ── URL collection ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _prompt_url() -> str | None:
|
|
31
|
+
"""Show URL input prompt. Returns None when the user quits.
|
|
32
|
+
|
|
33
|
+
Returns the sentinel ``"__settings__"`` when the user asks for settings.
|
|
34
|
+
"""
|
|
35
|
+
console.print()
|
|
36
|
+
print_rule()
|
|
37
|
+
console.print(
|
|
38
|
+
Panel(
|
|
39
|
+
Text.from_markup(
|
|
40
|
+
"[dim]Paste a YouTube URL and press[/] [bold cyan]Enter[/]\n"
|
|
41
|
+
"[dim]Type[/] [bold]s[/] [dim]for Settings ·[/] [bold]q[/] [dim]to quit[/]"
|
|
42
|
+
),
|
|
43
|
+
border_style="dim",
|
|
44
|
+
padding=(1, 4),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
console.print()
|
|
48
|
+
|
|
49
|
+
raw = Prompt.ask(" [bold cyan]URL[/]").strip()
|
|
50
|
+
if raw.lower() in {"q", "quit", "exit"}:
|
|
51
|
+
return None
|
|
52
|
+
if raw.lower() in {"s", "settings"}:
|
|
53
|
+
return "__settings__"
|
|
54
|
+
return raw
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── analysis ──────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _analyse_url(url: str) -> AnalysisResult | None:
|
|
61
|
+
"""
|
|
62
|
+
Run URL analysis under a Rich spinner.
|
|
63
|
+
Returns None and prints an error on failure.
|
|
64
|
+
"""
|
|
65
|
+
if not is_youtube_url(url):
|
|
66
|
+
console.print(
|
|
67
|
+
Panel(
|
|
68
|
+
Text.from_markup(
|
|
69
|
+
"[error]Not a recognised YouTube URL.[/]\n"
|
|
70
|
+
"[dim]Supported: youtube.com/watch, youtu.be, /shorts/, /playlist[/]"
|
|
71
|
+
),
|
|
72
|
+
border_style="red",
|
|
73
|
+
title="[error]Invalid URL[/]",
|
|
74
|
+
padding=(0, 2),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
with Status(
|
|
80
|
+
"[bold cyan]Analysing URL…[/]",
|
|
81
|
+
spinner="dots",
|
|
82
|
+
spinner_style="cyan",
|
|
83
|
+
console=console,
|
|
84
|
+
):
|
|
85
|
+
try:
|
|
86
|
+
return analyze(url)
|
|
87
|
+
except UnsupportedURLError as exc:
|
|
88
|
+
_print_error("Unsupported URL", str(exc))
|
|
89
|
+
return None
|
|
90
|
+
except AnalysisError as exc:
|
|
91
|
+
_print_error("Analysis Failed", str(exc))
|
|
92
|
+
return None
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
_print_error("Unexpected Error", str(exc))
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _print_error(title: str, body: str) -> None:
|
|
99
|
+
console.print(
|
|
100
|
+
Panel(
|
|
101
|
+
f"[error]{body}[/]",
|
|
102
|
+
title=f"[error]{title}[/]",
|
|
103
|
+
border_style="red",
|
|
104
|
+
padding=(0, 2),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── type-dispatch ─────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _show_menu(result: AnalysisResult) -> str | None:
|
|
113
|
+
if result.media_type == MediaType.PLAYLIST:
|
|
114
|
+
return show_playlist_menu(result)
|
|
115
|
+
return show_video_menu(result) # VIDEO and SHORTS share the same menu
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── action handler ────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _handle_action(action: str, result: AnalysisResult) -> None:
|
|
122
|
+
dispatch_wizard(action, result)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── main loop ─────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run() -> None:
|
|
129
|
+
console.clear()
|
|
130
|
+
print_banner()
|
|
131
|
+
|
|
132
|
+
from vidsmith.settings.store import reload_settings
|
|
133
|
+
|
|
134
|
+
reload_settings()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
while True:
|
|
138
|
+
try:
|
|
139
|
+
url = _prompt_url()
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
_goodbye()
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if url is None:
|
|
145
|
+
_goodbye()
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
if url == "__settings__":
|
|
149
|
+
dispatch_wizard("settings")
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
result = _analyse_url(url)
|
|
153
|
+
if result is None:
|
|
154
|
+
continue # bad URL → re-prompt
|
|
155
|
+
|
|
156
|
+
# Inner menu loop: stay on the same result until user goes Back or Quits
|
|
157
|
+
while True:
|
|
158
|
+
try:
|
|
159
|
+
action = _show_menu(result)
|
|
160
|
+
|
|
161
|
+
if action is None: # user chose Quit
|
|
162
|
+
_goodbye()
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if action == ACTION_BACK: # user chose Back → outer loop
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
_handle_action(action, result)
|
|
169
|
+
except KeyboardInterrupt:
|
|
170
|
+
console.print("\n [yellow]Action cancelled.[/]")
|
|
171
|
+
except KeyboardInterrupt:
|
|
172
|
+
_goodbye()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _goodbye() -> None:
|
|
176
|
+
console.print()
|
|
177
|
+
console.print(
|
|
178
|
+
Panel(
|
|
179
|
+
Text.from_markup("[bold cyan]Thanks for using VidSmith. Goodbye![/]"),
|
|
180
|
+
border_style="cyan",
|
|
181
|
+
padding=(0, 4),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
console.print()
|
|
File without changes
|
vidsmith/cli/doctor.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""``vidsmith doctor`` — environment diagnostics.
|
|
2
|
+
|
|
3
|
+
Inspects every optional and required tool VidSmith relies on and prints a
|
|
4
|
+
clear, colour-coded report with actionable install hints. Modelled on
|
|
5
|
+
``brew doctor`` / ``gh`` status output. It never raises: any probe that fails
|
|
6
|
+
is reported as a failed check rather than crashing the command.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import socket
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
from importlib.util import find_spec
|
|
18
|
+
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
from vidsmith.config import APP_NAME, APP_VERSION
|
|
24
|
+
from vidsmith.utils.console import console
|
|
25
|
+
|
|
26
|
+
_OK = "ok"
|
|
27
|
+
_WARN = "warn"
|
|
28
|
+
_FAIL = "fail"
|
|
29
|
+
|
|
30
|
+
_MARKS = {_OK: "[green]✓[/]", _WARN: "[yellow]⚠[/]", _FAIL: "[red]✗[/]"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class Check:
|
|
35
|
+
"""A single diagnostic row."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
status: str
|
|
39
|
+
detail: str = ""
|
|
40
|
+
hint: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── individual probes (each returns a Check, never raises) ────────────────────
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _check_python() -> Check:
|
|
47
|
+
version_text = platform.python_version()
|
|
48
|
+
if sys.version_info >= (3, 12):
|
|
49
|
+
return Check("Python", _OK, version_text)
|
|
50
|
+
return Check(
|
|
51
|
+
"Python",
|
|
52
|
+
_FAIL,
|
|
53
|
+
f"{version_text} (3.12+ required)",
|
|
54
|
+
"Install Python 3.12 or newer from https://python.org",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _check_package(
|
|
59
|
+
name: str,
|
|
60
|
+
module: str,
|
|
61
|
+
dist: str,
|
|
62
|
+
*,
|
|
63
|
+
required: bool,
|
|
64
|
+
hint: str,
|
|
65
|
+
) -> Check:
|
|
66
|
+
"""Report on an importable Python package by distribution name."""
|
|
67
|
+
spec_present = find_spec(module) is not None
|
|
68
|
+
if not spec_present:
|
|
69
|
+
return Check(name, _FAIL if required else _WARN, "Not installed", hint)
|
|
70
|
+
try:
|
|
71
|
+
detail = version(dist)
|
|
72
|
+
except PackageNotFoundError:
|
|
73
|
+
detail = "installed"
|
|
74
|
+
return Check(name, _OK, detail)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _check_binary(
|
|
78
|
+
name: str,
|
|
79
|
+
candidates: tuple[str, ...],
|
|
80
|
+
*,
|
|
81
|
+
required: bool,
|
|
82
|
+
hint: str,
|
|
83
|
+
) -> Check:
|
|
84
|
+
"""Report on an external executable resolved from PATH."""
|
|
85
|
+
for candidate in candidates:
|
|
86
|
+
path = shutil.which(candidate)
|
|
87
|
+
if path:
|
|
88
|
+
return Check(name, _OK, path)
|
|
89
|
+
return Check(name, _FAIL if required else _WARN, "Not installed", hint)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_ffmpeg() -> Check:
|
|
93
|
+
check = _check_binary(
|
|
94
|
+
"FFmpeg",
|
|
95
|
+
("ffmpeg",),
|
|
96
|
+
required=False,
|
|
97
|
+
hint="Install FFmpeg from https://ffmpeg.org (or `pip install imageio-ffmpeg`).",
|
|
98
|
+
)
|
|
99
|
+
if check.status != _OK:
|
|
100
|
+
# imageio-ffmpeg provides a bundled binary as a fallback.
|
|
101
|
+
try:
|
|
102
|
+
import imageio_ffmpeg
|
|
103
|
+
|
|
104
|
+
return Check("FFmpeg", _OK, f"bundled: {imageio_ffmpeg.get_ffmpeg_exe()}")
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return check
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_socket(host: str, name: str, ok_detail: str) -> Check:
|
|
111
|
+
try:
|
|
112
|
+
socket.setdefaulttimeout(6)
|
|
113
|
+
with socket.create_connection((host, 443)):
|
|
114
|
+
return Check(name, _OK, ok_detail)
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
return Check(
|
|
117
|
+
name, _WARN, f"Unreachable ({exc.__class__.__name__})", "Check your network connection."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def collect_checks(*, network: bool = True) -> list[Check]:
|
|
122
|
+
"""Run every diagnostic and return the ordered result list."""
|
|
123
|
+
checks: list[Check] = [
|
|
124
|
+
_check_python(),
|
|
125
|
+
_check_package(
|
|
126
|
+
"yt-dlp",
|
|
127
|
+
"yt_dlp",
|
|
128
|
+
"yt-dlp",
|
|
129
|
+
required=True,
|
|
130
|
+
hint="Reinstall VidSmith or run `pip install -U yt-dlp`.",
|
|
131
|
+
),
|
|
132
|
+
_check_ffmpeg(),
|
|
133
|
+
_check_package(
|
|
134
|
+
"curl_cffi",
|
|
135
|
+
"curl_cffi",
|
|
136
|
+
"curl_cffi",
|
|
137
|
+
required=False,
|
|
138
|
+
hint="`pip install curl_cffi` — enables browser impersonation and reduces HTTP 429.",
|
|
139
|
+
),
|
|
140
|
+
_check_package(
|
|
141
|
+
"mutagen",
|
|
142
|
+
"mutagen",
|
|
143
|
+
"mutagen",
|
|
144
|
+
required=False,
|
|
145
|
+
hint="`pip install mutagen` — writes MP4/M4A cover-art atoms visible in Explorer.",
|
|
146
|
+
),
|
|
147
|
+
_check_binary(
|
|
148
|
+
"AtomicParsley",
|
|
149
|
+
("AtomicParsley", "atomicparsley"),
|
|
150
|
+
required=False,
|
|
151
|
+
hint="Optional alternative to mutagen for MP4 cover art.",
|
|
152
|
+
),
|
|
153
|
+
_check_binary(
|
|
154
|
+
"Node.js",
|
|
155
|
+
("node",),
|
|
156
|
+
required=False,
|
|
157
|
+
hint="Optional JavaScript runtime; Deno is preferred.",
|
|
158
|
+
),
|
|
159
|
+
_check_binary(
|
|
160
|
+
"Deno",
|
|
161
|
+
("deno",),
|
|
162
|
+
required=False,
|
|
163
|
+
hint="`https://deno.com` — recommended JS runtime for full YouTube format parity.",
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
if network:
|
|
167
|
+
checks.append(_check_socket("one.one.one.one", "Internet", "Connected"))
|
|
168
|
+
checks.append(_check_socket("www.youtube.com", "YouTube Access", "Reachable"))
|
|
169
|
+
return checks
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def run_doctor(*, network: bool = True) -> int:
|
|
173
|
+
"""Render the diagnostics report. Returns a process exit code.
|
|
174
|
+
|
|
175
|
+
Exit code is non-zero only when a *required* check fails, so scripts and CI
|
|
176
|
+
can gate on it; optional-tool warnings keep the exit code at 0.
|
|
177
|
+
"""
|
|
178
|
+
checks = collect_checks(network=network)
|
|
179
|
+
|
|
180
|
+
table = Table.grid(padding=(0, 2))
|
|
181
|
+
table.add_column(justify="center", no_wrap=True)
|
|
182
|
+
table.add_column(style="bold", no_wrap=True)
|
|
183
|
+
table.add_column(style="white")
|
|
184
|
+
for check in checks:
|
|
185
|
+
table.add_row(_MARKS.get(check.status, "?"), check.name, check.detail)
|
|
186
|
+
|
|
187
|
+
console.print()
|
|
188
|
+
console.print(
|
|
189
|
+
Panel(
|
|
190
|
+
table,
|
|
191
|
+
title=f"[bold cyan] {APP_NAME} Doctor · v{APP_VERSION} [/]",
|
|
192
|
+
border_style="cyan",
|
|
193
|
+
padding=(1, 2),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
hints = [check for check in checks if check.status != _OK and check.hint]
|
|
198
|
+
if hints:
|
|
199
|
+
lines = [
|
|
200
|
+
f"{_MARKS[check.status]} [bold]{check.name}[/] — {check.detail or 'unavailable'}\n"
|
|
201
|
+
f" [dim]{check.hint}[/]"
|
|
202
|
+
for check in hints
|
|
203
|
+
]
|
|
204
|
+
console.print(
|
|
205
|
+
Panel(
|
|
206
|
+
Text.from_markup("\n".join(lines)),
|
|
207
|
+
title="[yellow] Recommendations [/]",
|
|
208
|
+
border_style="yellow",
|
|
209
|
+
padding=(1, 2),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
console.print("[green]Everything looks good — VidSmith is fully equipped.[/]\n")
|
|
214
|
+
|
|
215
|
+
has_required_failure = any(
|
|
216
|
+
check.status == _FAIL and check.name in {"Python", "yt-dlp"} for check in checks
|
|
217
|
+
)
|
|
218
|
+
return 1 if has_required_failure else 0
|