ManiAz09 commited on
Commit
04ec20b
·
verified ·
1 Parent(s): f8ecbe1

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.13
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VaslNistim
3
+ emoji: 🦀
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
__pycache__/video_dl.cpython-313.pyc ADDED
Binary file (19.2 kB). View file
 
__pycache__/video_dl.cpython-314.pyc ADDED
Binary file (11.4 kB). View file
 
app.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from threading import Thread
2
+ import gradio as gr
3
+ import speedtest
4
+ import platform
5
+ import psutil
6
+ import os
7
+
8
+
9
+ def starter():
10
+ os.system("python3 Kmain.py")
11
+
12
+
13
+ thread = Thread(target=starter)
14
+ thread.start()
15
+
16
+
17
+ def get_system_usage():
18
+ # Get CPU usage as a percentage
19
+ cpu_usage = psutil.cpu_percent(interval=1)
20
+
21
+ # Get memory usage
22
+ memory = psutil.virtual_memory()
23
+ total_memory = memory.total
24
+ available_memory = memory.available
25
+ memory_usage = total_memory - available_memory
26
+
27
+ # Get disk usage
28
+ disk = psutil.disk_usage('/')
29
+ total_disk = disk.total
30
+ used_disk = disk.used
31
+ disk_usage = used_disk
32
+
33
+ # Prepare the system usage information as a string
34
+ system_usage = f"CPU Usage: {cpu_usage}%\n"
35
+ system_usage += f"Memory Usage: {memory_usage} bytes\n"
36
+ system_usage += f"Disk Usage: {disk_usage} bytes"
37
+
38
+ return system_usage
39
+
40
+
41
+ def get_internet_speed():
42
+ st = speedtest.Speedtest()
43
+ st.get_best_server()
44
+ download_speed = st.download() / 1_000_000 / 8 # Convert to MBps
45
+ upload_speed = st.upload() / 1_000_000 / 8 # Convert to MBps
46
+ return download_speed, upload_speed
47
+
48
+
49
+ def get_system_information():
50
+ system = platform.system()
51
+ release = platform.release()
52
+ version = platform.version()
53
+ machine = platform.machine()
54
+ processor = platform.processor()
55
+
56
+ memory = psutil.virtual_memory()
57
+ memory_total = memory.total
58
+ memory_used = memory.used
59
+ memory_percent = memory.percent
60
+
61
+ disk = psutil.disk_usage('/')
62
+ disk_total = disk.total
63
+ disk_used = disk.used
64
+ disk_percent = disk.percent
65
+
66
+ cpu_percent = psutil.cpu_percent()
67
+
68
+ download_speed, upload_speed = get_internet_speed()
69
+
70
+ information = (f"System Information:\n"
71
+ f"- System: {system}\n"
72
+ f"- Release: {release}\n"
73
+ f"- Version: {version}\n"
74
+ f"- Machine: {machine}\n"
75
+ f"- Processor: {processor}\n"
76
+ f"\n"
77
+ f"Memory Usage:\n"
78
+ f"- Memory Total: {memory_total} bytes\n"
79
+ f"- Memory Used: {memory_used} bytes\n"
80
+ f"- Memory Percent: {memory_percent}%\n"
81
+ f"\n"
82
+ f"Disk Usage:\n"
83
+ f"- Disk Total: {disk_total} bytes\n"
84
+ f"- Disk Used: {disk_used} bytes\n"
85
+ f"- Disk Percent: {disk_percent}%\n"
86
+ f"\n"
87
+ f"CPU Usage:\n"
88
+ f"- CPU Percent: {cpu_percent}%\n"
89
+ f"\n"
90
+ f"- Internet Usage:\n"
91
+ f"Download Speed: {download_speed:.2f} MBps\n"
92
+ f"Upload Speed: {upload_speed:.2f} MBps"
93
+ )
94
+
95
+ return information
96
+
97
+
98
+
99
+ iface = gr.Interface(fn=get_system_information, inputs=None, outputs="text")
100
+ iface.launch(server_name="0.0.0.0", server_port=7860)
main.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from video_dl import get_video_formats, download_video
3
+ from balethon import Client, conditions
4
+ from balethon.objects.message import Message
5
+ import json
6
+ import re
7
+
8
+
9
+ url = "https://www.youtube.com/watch?v=GVPhBSmMuq8&pp=ugUEEgJlbg%3D%3D"
10
+ data = get_video_formats(url=url)
11
+ # print(json.dumps(data, indent=2))
12
+ # download_video(url=url, format_id="278", output="video.mp4", ffmpeg_location=r"C:\ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe")
13
+
14
+
15
+
16
+ bot = Client(os.environ["BotToken"])
17
+
18
+ def is_youtube_link(event: str) -> bool:
19
+ link = False
20
+ if event.text:
21
+ # Regular expression to match various YouTube URL formats
22
+ youtube_regex = r'^(https?://)?(www\.|m\.)?(youtube\.com|youtu\.be)/'
23
+ if re.match(youtube_regex, event.text):
24
+ link = True
25
+ return link
26
+
27
+
28
+
29
+ @bot.on_message(condition=conditions.Condition(is_youtube_link))
30
+ async def greet(message: Message):
31
+ user_message = message.text
32
+ formats = get_video_formats(url=user_message)
33
+ output_path = download_video(url=user_message, format_id=formats[-1]["format_id"])
34
+ # await message.reply("Hello")
35
+ await message.reply_video(output_path)
36
+
37
+ bot.run()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ h2
2
+ gradio-client
3
+ gradio
4
+ speedtest
5
+ cryptography
6
+ psutil
7
+ yt-dlp
video_dl.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video format extraction and download utilities using yt-dlp.
3
+
4
+ Provides two core functions:
5
+ - get_video_formats: Extract available video format metadata, deduplicated
6
+ by resolution+FPS with largest file size per combination
7
+ - download_video: Download a specific video format by ID with automatic
8
+ best audio merging for video-only formats.
9
+
10
+ Both functions support custom ffmpeg location for format merging.
11
+
12
+ Requires yt-dlp to be installed: pip install yt-dlp
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from pathlib import Path
19
+ from typing import Any, TypedDict
20
+
21
+ import yt_dlp
22
+ from yt_dlp.utils import DownloadError, ExtractorError
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class VideoFormatInfo(TypedDict):
28
+ """Structure for video format metadata returned by get_video_formats."""
29
+ format_id: str
30
+ width: int | None
31
+ height: int | None
32
+ size: int | None
33
+ fps: float | None
34
+ duration: float | None
35
+
36
+
37
+ class VideoDownloadError(Exception):
38
+ """Base exception for video download operations."""
39
+ pass
40
+
41
+
42
+ class FormatExtractionError(VideoDownloadError):
43
+ """Raised when format extraction fails."""
44
+ pass
45
+
46
+
47
+ class DownloadFailedError(VideoDownloadError):
48
+ """Raised when the actual download fails."""
49
+ pass
50
+
51
+
52
+ class InvalidURLError(VideoDownloadError):
53
+ """Raised when the provided URL is invalid or unsupported."""
54
+ pass
55
+
56
+
57
+ class FormatNotFoundError(VideoDownloadError):
58
+ """Raised when the requested format ID does not exist."""
59
+ pass
60
+
61
+
62
+ class FFmpegNotFoundError(VideoDownloadError):
63
+ """Raised when the specified ffmpeg binary does not exist."""
64
+ pass
65
+
66
+
67
+ def _validate_url(url: str) -> None:
68
+ """Validate that the URL has an acceptable scheme.
69
+
70
+ Args:
71
+ url: The URL string to validate.
72
+
73
+ Raises:
74
+ InvalidURLError: If the URL scheme is not http or https.
75
+ """
76
+ from urllib.parse import urlparse
77
+
78
+ parsed = urlparse(url)
79
+ if parsed.scheme not in ("http", "https"):
80
+ raise InvalidURLError(
81
+ f"Unsupported URL scheme '{parsed.scheme}'. "
82
+ f"Only http and https are allowed. Got: {url}"
83
+ )
84
+ if not parsed.netloc:
85
+ raise InvalidURLError(f"URL has no hostname. Got: {url}")
86
+
87
+
88
+ def _sanitize_output_path(output: str | Path | None) -> Path | None:
89
+ """Sanitize and resolve the output path.
90
+
91
+ Args:
92
+ output: User-provided output path string or Path object, or None.
93
+
94
+ Returns:
95
+ Resolved absolute Path object, or None if output is None.
96
+
97
+ Raises:
98
+ ValueError: If the path contains directory traversal attempts
99
+ or is otherwise invalid.
100
+ """
101
+ if output is None:
102
+ return None
103
+
104
+ path = Path(output).expanduser().resolve()
105
+
106
+ # Basic path traversal protection — resolve should handle .. but
107
+ # ensure the resolved path is still within reasonable bounds
108
+ try:
109
+ path.relative_to(path.anchor or "/")
110
+ except ValueError:
111
+ raise ValueError(f"Invalid output path: {output}")
112
+
113
+ # Create parent directories if they don't exist
114
+ path.parent.mkdir(parents=True, exist_ok=True)
115
+
116
+ return path
117
+
118
+
119
+ def _validate_ffmpeg_path(ffmpeg_location: str | Path | None) -> str | None:
120
+ """Validate and resolve the ffmpeg binary path.
121
+
122
+ Args:
123
+ ffmpeg_location: Path to ffmpeg executable, or None to use
124
+ system default.
125
+
126
+ Returns:
127
+ Resolved absolute path as string, or None for system default.
128
+
129
+ Raises:
130
+ FFmpegNotFoundError: If the specified path does not exist or
131
+ is not a file.
132
+ """
133
+ if ffmpeg_location is None:
134
+ return None
135
+
136
+ ffmpeg_path = Path(ffmpeg_location).expanduser().resolve()
137
+
138
+ if not ffmpeg_path.exists():
139
+ raise FFmpegNotFoundError(
140
+ f"ffmpeg binary not found at: {ffmpeg_path}"
141
+ )
142
+ if not ffmpeg_path.is_file():
143
+ raise FFmpegNotFoundError(
144
+ f"ffmpeg path exists but is not a file: {ffmpeg_path}"
145
+ )
146
+
147
+ logger.debug("Using custom ffmpeg at: %s", ffmpeg_path)
148
+ return str(ffmpeg_path)
149
+
150
+
151
+ def _build_base_ydl_opts(
152
+ ffmpeg_location: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ """Build the base yt-dlp options dictionary.
155
+
156
+ Args:
157
+ ffmpeg_location: Optional path to ffmpeg binary. If None, yt-dlp
158
+ will search the system PATH.
159
+
160
+ Returns:
161
+ Base options dictionary for yt-dlp configuration.
162
+ """
163
+ opts: dict[str, Any] = {
164
+ "quiet": True,
165
+ "no_warnings": True,
166
+ "noplaylist": True,
167
+ "compat_opts": ["no-certifi"],
168
+ }
169
+
170
+ if ffmpeg_location is not None:
171
+ opts["ffmpeg_location"] = ffmpeg_location
172
+
173
+ return opts
174
+
175
+
176
+ def _get_deduplication_key(
177
+ fmt: dict[str, Any],
178
+ ) -> tuple[int | None, int | None, float | None]:
179
+ """Extract the deduplication key (width, height, fps) from a format dict.
180
+
181
+ Args:
182
+ fmt: A raw format dictionary from yt-dlp.
183
+
184
+ Returns:
185
+ A tuple of (width, height, fps) used for grouping similar formats.
186
+ """
187
+ return (
188
+ fmt.get("width"),
189
+ fmt.get("height"),
190
+ fmt.get("fps"),
191
+ )
192
+
193
+
194
+ def _deduplicate_formats_by_largest_size(
195
+ formats: list[dict[str, Any]],
196
+ ) -> list[dict[str, Any]]:
197
+ """Deduplicate formats by resolution+FPS, keeping the largest size variant.
198
+
199
+ Audio-only formats (null width and height) are removed entirely.
200
+ Within each (width, height, fps) group, the format with the largest
201
+ non-null size is kept. If all formats in a group have null size,
202
+ the first one encountered is retained.
203
+
204
+ Args:
205
+ formats: List of raw format dictionaries from yt-dlp.
206
+
207
+ Returns:
208
+ Deduplicated list of format dictionaries, sorted by resolution
209
+ descending, then by size descending.
210
+ """
211
+ # Step 1: Remove audio-only formats and unavailable formats
212
+ video_formats = [
213
+ fmt for fmt in formats
214
+ if fmt.get("width") is not None
215
+ and fmt.get("height") is not None
216
+ and fmt.get("availability") != "unavailable"
217
+ ]
218
+
219
+ if not video_formats:
220
+ return []
221
+
222
+ # Step 2: Group by (width, height, fps) and keep largest size
223
+ grouped: dict[
224
+ tuple[int | None, int | None, float | None], dict[str, Any]
225
+ ] = {}
226
+
227
+ for fmt in video_formats:
228
+ key = _get_deduplication_key(fmt)
229
+ current_size = fmt.get("filesize") or fmt.get("filesize_approx")
230
+
231
+ if key not in grouped:
232
+ grouped[key] = fmt
233
+ else:
234
+ existing_size = (
235
+ grouped[key].get("filesize")
236
+ or grouped[key].get("filesize_approx")
237
+ )
238
+
239
+ # If current format has a known size and existing doesn't,
240
+ # or current size is larger — replace
241
+ if current_size is not None:
242
+ if existing_size is None or current_size > existing_size:
243
+ grouped[key] = fmt
244
+ # If both are None, keep the first one (already in grouped)
245
+
246
+ # Step 3: Sort by resolution descending, then size descending
247
+ def sort_key(
248
+ item: tuple[
249
+ tuple[int | None, int | None, float | None], dict[str, Any]
250
+ ],
251
+ ) -> tuple[int, int, int]:
252
+ (width, height, __), fmt = item
253
+ # Use 0 for None values to place them at the end
254
+ resolved_width = width if width is not None else 0
255
+ resolved_height = height if height is not None else 0
256
+ size = fmt.get("filesize") or fmt.get("filesize_approx") or 0
257
+ # Sort by width descending, then height descending, then size descending
258
+ return (-resolved_width, -resolved_height, -size)
259
+
260
+ sorted_grouped = sorted(grouped.items(), key=sort_key)
261
+
262
+ return [fmt for _, fmt in sorted_grouped]
263
+
264
+
265
+ def get_video_formats(
266
+ url: str,
267
+ ffmpeg_location: str | Path | None = None,
268
+ ) -> list[VideoFormatInfo]:
269
+ """Extract available video format metadata from a URL using yt-dlp.
270
+
271
+ Fetches format information without downloading the video. Formats are
272
+ deduplicated by unique (width, height, fps) combinations, keeping only
273
+ the variant with the largest file size for each combination. Audio-only
274
+ formats are excluded. Results are sorted by resolution descending.
275
+
276
+ Args:
277
+ url: The video URL to extract formats from. Must use http or https.
278
+ ffmpeg_location: Optional path to ffmpeg executable. If provided,
279
+ will be used for any format merging operations.
280
+ If None, yt-dlp searches the system PATH.
281
+ Example: "C:\\ffmpeg-master-latest-win64-gpl\\bin\\ffmpeg.exe"
282
+
283
+ Returns:
284
+ A list of VideoFormatInfo dictionaries, one per unique
285
+ (width, height, fps) combination, sorted by resolution descending.
286
+ Returns an empty list if no video formats are found.
287
+
288
+ Raises:
289
+ InvalidURLError: If the URL scheme is not http/https.
290
+ FFmpegNotFoundError: If the specified ffmpeg_location does not exist.
291
+ FormatExtractionError: If extraction fails for any reason
292
+ (network error, unsupported site, etc).
293
+
294
+ Example:
295
+ >>> formats = get_video_formats(
296
+ ... "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
297
+ ... ffmpeg_location="C:\\\\ffmpeg\\\\bin\\\\ffmpeg.exe"
298
+ ... )
299
+ >>> for fmt in formats:
300
+ ... print(f"{fmt['width']}x{fmt['height']} @ {fmt['fps']}fps")
301
+ """
302
+ _validate_url(url)
303
+ resolved_ffmpeg = _validate_ffmpeg_path(ffmpeg_location)
304
+
305
+ ydl_opts = _build_base_ydl_opts(ffmpeg_location=resolved_ffmpeg)
306
+ ydl_opts["extract_flat"] = False
307
+ ydl_opts["skip_download"] = True
308
+ ydl_opts["cookiefile"] = None
309
+
310
+ try:
311
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
312
+ info = ydl.extract_info(url, download=False)
313
+ except (DownloadError, ExtractorError) as exc:
314
+ raise FormatExtractionError(
315
+ f"Failed to extract formats from URL: {exc}"
316
+ ) from exc
317
+ except Exception as exc:
318
+ raise FormatExtractionError(
319
+ f"Unexpected error during format extraction: {exc}"
320
+ ) from exc
321
+
322
+ if info is None:
323
+ raise FormatExtractionError(
324
+ f"No information could be extracted from: {url}"
325
+ )
326
+
327
+ formats_raw: list[dict[str, Any]] = info.get("formats") or []
328
+
329
+ # Deduplicate: keep only largest size per (width, height, fps) group
330
+ deduplicated = _deduplicate_formats_by_largest_size(formats_raw)
331
+
332
+ result: list[VideoFormatInfo] = []
333
+ for fmt in deduplicated:
334
+ if type(fmt.get("fps")) == float:
335
+ continue
336
+ format_info: VideoFormatInfo = {
337
+ "format_id": str(fmt.get("format_id", "")),
338
+ "width": fmt.get("width"),
339
+ "height": fmt.get("height"),
340
+ "size": fmt.get("filesize") or fmt.get("filesize_approx"),
341
+ "fps": fmt.get("fps"),
342
+ "duration": info.get("duration"),
343
+ }
344
+ result.append(format_info)
345
+
346
+ return result
347
+
348
+
349
+ def _format_has_audio(format_info: dict[str, Any]) -> bool:
350
+ """Determine if a format entry already contains an audio stream.
351
+
352
+ Checks audio codec presence and video codec absence to identify
353
+ whether the format includes audio. Formats with no video codec
354
+ are audio-only, and formats with an audio codec are muxed.
355
+
356
+ Args:
357
+ format_info: A single format dictionary from yt-dlp extraction.
358
+
359
+ Returns:
360
+ True if the format contains audio, False if it is video-only.
361
+ """
362
+ # Audio-only formats have no video codec
363
+ if format_info.get("vcodec") == "none":
364
+ return True
365
+
366
+ # Muxed formats have an audio codec present
367
+ if format_info.get("acodec") and format_info["acodec"] != "none":
368
+ return True
369
+
370
+ # Video-only formats have vcodec but no acodec (or acodec="none")
371
+ return False
372
+
373
+
374
+ def download_video(
375
+ url: str,
376
+ format_id: str,
377
+ output: str | Path | None = None,
378
+ ffmpeg_location: str | Path | None = None,
379
+ ) -> str:
380
+ """Download a video in the specified format using yt-dlp.
381
+
382
+ Downloads the video with the given format ID. If the requested format
383
+ is video-only (no audio track), the best available audio is
384
+ automatically selected and merged into the final output using ffmpeg.
385
+
386
+ If an output path is provided, the file will be saved to that exact
387
+ location. Otherwise, yt-dlp's default naming template is used.
388
+
389
+ Args:
390
+ url: The video URL to download. Must use http or https.
391
+ format_id: The yt-dlp format ID string (e.g., "137", "22").
392
+ Video-only formats will automatically have audio
393
+ merged in.
394
+ output: Optional output file path. Directories will be created
395
+ if they don't exist. Should include the file extension.
396
+ ffmpeg_location: Optional path to ffmpeg executable. Required
397
+ if system doesn't have ffmpeg in PATH. Used for
398
+ merging video and audio streams.
399
+ Example: "C:\\\\ffmpeg-master-latest-win64-gpl\\\\bin\\\\ffmpeg.exe"
400
+
401
+ Returns:
402
+ The absolute path (as a string) where the video was saved.
403
+
404
+ Raises:
405
+ InvalidURLError: If the URL scheme is not http/https.
406
+ FFmpegNotFoundError: If the specified ffmpeg_location does not exist.
407
+ FormatNotFoundError: If the specified format_id is not found.
408
+ DownloadFailedError: If the download fails for any reason.
409
+ ValueError: If the output path is invalid.
410
+
411
+ Example:
412
+ >>> path = download_video(
413
+ ... "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
414
+ ... format_id="137",
415
+ ... output="/tmp/video.mp4",
416
+ ... ffmpeg_location="C:\\\\ffmpeg\\\\bin\\\\ffmpeg.exe"
417
+ ... )
418
+ >>> print(f"Saved to: {path}")
419
+ """
420
+ _validate_url(url)
421
+ sanitized_output = _sanitize_output_path(output)
422
+ resolved_ffmpeg = _validate_ffmpeg_path(ffmpeg_location)
423
+
424
+ # Extract format metadata to check for audio and validate format_id
425
+ try:
426
+ ydl_opts = _build_base_ydl_opts(ffmpeg_location=resolved_ffmpeg)
427
+ ydl_opts["extract_flat"] = False
428
+ ydl_opts["skip_download"] = True
429
+
430
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
431
+ info = ydl.extract_info(url, download=False)
432
+ except (DownloadError, ExtractorError) as exc:
433
+ raise DownloadFailedError(
434
+ f"Cannot verify format availability: {exc}"
435
+ ) from exc
436
+ except Exception as exc:
437
+ raise DownloadFailedError(
438
+ f"Unexpected error during format check: {exc}"
439
+ ) from exc
440
+
441
+ if info is None:
442
+ raise DownloadFailedError(
443
+ "No information could be extracted from the URL."
444
+ )
445
+
446
+ # Find the requested format in available formats
447
+ formats_raw: list[dict[str, Any]] = info.get("formats") or []
448
+ available_ids = {str(fmt.get("format_id", "")) for fmt in formats_raw}
449
+
450
+ if format_id not in available_ids:
451
+ raise FormatNotFoundError(
452
+ f"Format ID '{format_id}' not found. "
453
+ f"Available IDs: {sorted(available_ids)}"
454
+ )
455
+
456
+ # Determine if we need to add audio
457
+ selected_format = next(
458
+ fmt for fmt in formats_raw
459
+ if str(fmt.get("format_id", "")) == format_id
460
+ )
461
+
462
+ if _format_has_audio(selected_format):
463
+ # Format already has audio — use as-is
464
+ final_format = format_id
465
+ logger.info(
466
+ "Format '%s' already includes audio. Downloading as-is.",
467
+ format_id,
468
+ )
469
+ else:
470
+ # Video-only format — merge with best audio
471
+ final_format = f"{format_id}+bestaudio"
472
+ logger.info(
473
+ "Format '%s' is video-only. Merging with best audio → '%s'.",
474
+ format_id,
475
+ final_format,
476
+ )
477
+
478
+ # Build yt-dlp options for download
479
+ ydl_opts = _build_base_ydl_opts(ffmpeg_location=resolved_ffmpeg)
480
+ ydl_opts["format"] = final_format
481
+ ydl_opts["merge_output_format"] = "mp4"
482
+ ydl_opts["overwrites"] = True
483
+
484
+ # If user specifies an output path, use it exactly
485
+ if sanitized_output is not None:
486
+ ydl_opts["outtmpl"] = str(sanitized_output)
487
+ ydl_opts["paths"] = {}
488
+
489
+ try:
490
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
491
+ info = ydl.extract_info(url, download=True)
492
+ except (DownloadError, ExtractorError) as exc:
493
+ raise DownloadFailedError(
494
+ f"Download failed for format '{final_format}': {exc}"
495
+ ) from exc
496
+ except Exception as exc:
497
+ raise DownloadFailedError(
498
+ f"Unexpected error during download: {exc}"
499
+ ) from exc
500
+
501
+ if info is None:
502
+ raise DownloadFailedError(
503
+ "Download completed but no information was returned."
504
+ )
505
+
506
+ # Determine the actual output path
507
+ if sanitized_output is not None:
508
+ final_path = sanitized_output
509
+ else:
510
+ requested_downloads = info.get("requested_downloads") or []
511
+ if requested_downloads:
512
+ final_path = Path(requested_downloads[0].get("filepath", ""))
513
+ else:
514
+ final_path = Path(ydl.prepare_filename(info))
515
+
516
+ final_path = final_path.resolve()
517
+
518
+ if not final_path.exists():
519
+ raise DownloadFailedError(
520
+ f"Download appeared to succeed but file not found at: "
521
+ f"{final_path}"
522
+ )
523
+
524
+ logger.info("Video downloaded successfully to: %s", final_path)
525
+ return str(final_path)