udown 0.0.2__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,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: udown
3
+ Version: 0.0.2
4
+ Summary: A fast YouTube video downloader with parallel processing
5
+ Author-email: Dipanjal Maitra <dipanjalmaitra@gmail.com>
6
+ Maintainer-email: Dipanjal Maitra <dipanjalmaitra@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/dipanjal/udown
9
+ Project-URL: Documentation, https://github.com/dipanjal/udown#readme
10
+ Project-URL: Repository, https://github.com/dipanjal/udown
11
+ Project-URL: Bug Tracker, https://github.com/dipanjal/udown/issues
12
+ Keywords: youtube,download,video,audio,parallel,processing
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: pytubefix==9.4.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest~=7.1.2; extra == "dev"
29
+ Requires-Dist: pytest-cov~=4.0.0; extra == "dev"
30
+ Requires-Dist: black~=24.3.0; extra == "dev"
31
+ Requires-Dist: pylint~=3.1.0; extra == "dev"
32
+ Requires-Dist: mypy~=1.9.0; extra == "dev"
33
+ Requires-Dist: build~=1.0.0; extra == "dev"
34
+ Requires-Dist: twine~=6.0.0; extra == "dev"
35
+ Requires-Dist: isort~=5.0.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # UDown
39
+
40
+ A fast YouTube video downloader with parallel processing capabilities. Downloads audio and video streams simultaneously for optimal performance.
41
+
42
+ [![codecov](https://codecov.io/gh/dipanjal/udown/branch/main/graph/badge.svg)](https://codecov.io/gh/dipanjal/udown)
43
+
44
+ ## Features
45
+
46
+ - ⚡ **Parallel Processing**: Downloads audio and video streams simultaneously
47
+ - 📊 **Performance Profiling**: Detailed timing reports for download operations
48
+ - 🎯 **High Quality**: Downloads best available audio and video streams
49
+ - 📝 **Caption Support**: Optional subtitle/caption downloads
50
+ - 🔧 **FFmpeg Integration**: Automatic audio-video merging
51
+ - 🖥️ **Cross-Platform**: Works on Windows, macOS, and Linux
52
+
53
+ ## Installation
54
+
55
+ ### From PyPI (Recommended)
56
+
57
+ ```bash
58
+ pip install udown
59
+ ```
60
+
61
+ ### From Source
62
+
63
+ ```bash
64
+ git clone https://github.com/dipanjal/udown.git
65
+ cd udown
66
+ pip install -e .
67
+ ```
68
+
69
+ ## Prerequisites
70
+
71
+ - **Python 3.8+**
72
+ - **FFmpeg**: Required for audio-video merging
73
+
74
+ ### Installing FFmpeg
75
+
76
+ #### macOS
77
+ ```bash
78
+ brew install ffmpeg
79
+ ```
80
+
81
+ #### Ubuntu/Debian
82
+ ```bash
83
+ sudo apt update
84
+ sudo apt install ffmpeg
85
+ ```
86
+
87
+ #### Windows
88
+ Download from [FFmpeg official website](https://ffmpeg.org/download.html) or install via Chocolatey:
89
+ ```bash
90
+ choco install ffmpeg
91
+ ```
92
+
93
+ ## Usage
94
+
95
+ ### Basic Usage
96
+
97
+ ```bash
98
+ udown -o ~/Downloads [youtube_url]
99
+ ```
100
+
101
+ ### Advanced Usage
102
+
103
+ ```bash
104
+ # Download to custom directory
105
+ udown -o ~/Downloads [youtube_url]
106
+
107
+ # Download with captions
108
+ udown -c -o ~/Downloads [youtube_url]
109
+
110
+ # Enable debug mode with detailed timing
111
+ udown -d -o ~/Downloads [youtube_url]
112
+
113
+ # Combine options
114
+ udown -o ~/Videos -c -d [youtube_url]
115
+ ```
116
+
117
+ ### Command Line Options
118
+
119
+ | Option | Description |
120
+ |--------|-------------|
121
+ | `-o, --output` | Output directory (default: `./downloads`) |
122
+ | `-c, --caption` | Download captions/subtitles if available |
123
+ | `-d, --debug` | Enable debug mode with detailed timing |
124
+ | `--version` | Show version information |
125
+ | `-h, --help` | Show help message |
126
+
127
+ ### Examples
128
+
129
+ ```bash
130
+ # Download a video
131
+ udown -o ~/Downloads https://www.youtube.com/watch?v=dQw4w9WgXcQ
132
+
133
+ # Download with captions to Downloads folder
134
+ udown -c -o ~/Downloads https://www.youtube.com/watch?v=dQw4w9WgXcQ
135
+
136
+ # Debug mode to see performance metrics
137
+ udown -d -o ~/Downloads https://www.youtube.com/watch?v=dQw4w9WgXcQ
138
+ ```
139
+
140
+ ## Performance
141
+
142
+ The parallel processing approach provides significant time savings:
143
+
144
+ - **Sequential Download**: Audio + Video + Merge = Total Time
145
+ - **Parallel Download**: Max(Audio, Video) + Merge = Reduced Time
146
+
147
+ ### Sample Output
148
+
149
+ ```
150
+ Downloading: Example Video Title
151
+ Downloading Audio File
152
+ Downloading Video File
153
+ Video downloaded successfully to: ./downloads/Example Video Title.mp4
154
+
155
+ ==================================================
156
+ DOWNLOAD PERFORMANCE REPORT
157
+ ==================================================
158
+ Audio Took: 12.34 seconds
159
+ Video Took: 15.67 seconds
160
+ Merging Took: 2.45 seconds
161
+ Expected Duration: 30.46 seconds
162
+ Total Process Time: 18.23 seconds
163
+ ==================================================
164
+ Time Saved: 12.23 seconds
165
+ ==================================================
166
+ ```
167
+
168
+ ## Development
169
+
170
+ ### Setup Development Environment
171
+
172
+ ```bash
173
+ git clone https://github.com/dipanjal/udown.git
174
+ cd udown
175
+ make install
176
+ ```
177
+
178
+ ### Running Tests
179
+
180
+ ```bash
181
+ pytest
182
+ ```
183
+
184
+ ### Code Formatting
185
+
186
+ ```bash
187
+ black ytdl/
188
+ ```
189
+
190
+ ## Architecture
191
+
192
+ ```
193
+ udown/
194
+ ├── ytdl/
195
+ │ ├── __init__.py # Package initialization
196
+ │ ├── cli.py # Command-line interface
197
+ │ ├── downloader.py # Core download logic
198
+ │ ├── profiler.py # Performance timing
199
+ │ └── utils.py # Utility functions
200
+ ├── setup.py # Package setup
201
+ ├── pyproject.toml # Modern packaging config
202
+ └── README.md # This file
203
+ ```
204
+
205
+ ## How It Works
206
+
207
+ 1. **URL Validation**: Validates YouTube URL format
208
+ 2. **Stream Analysis**: Identifies best audio and video streams
209
+ 3. **Parallel Downloads**: Downloads audio and video simultaneously
210
+ 4. **FFmpeg Merge**: Combines streams into final video file
211
+ 5. **Performance Profiling**: Tracks timing for optimization
212
+
213
+ ## Contributing
214
+
215
+ 1. Fork the repository
216
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
217
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
218
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
219
+ 5. Open a Pull Request
220
+
221
+ ## License
222
+
223
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
224
+
225
+ ## Acknowledgments
226
+
227
+ - [pytubefix](https://github.com/pytubefix/pytubefix) - YouTube data extraction
228
+ - [FFmpeg](https://ffmpeg.org/) - Audio/video processing
229
+
230
+ ## Support
231
+
232
+ - 📧 **Email**: dipanjalmaitra@gmail.com
233
+ - 🐛 **Issues**: [GitHub Issues](https://github.com/dipanjalmaitra/udown/issues)
234
+ - 📖 **Documentation**: [GitHub Wiki](https://github.com/dipanjalmaitra/udown/wiki)
@@ -0,0 +1,11 @@
1
+ udown-0.0.2.dist-info/licenses/LICENSE,sha256=BNnWV5PsQHnuMltsEKuIuFsAHkR_ZUZWH3QAWVdenh0,1072
2
+ ytdl/__init__.py,sha256=GGAGfModazmRoKb-I2Bauc5oY2FqjN0mrtR0m3_zsQw,297
3
+ ytdl/cli.py,sha256=ExdVy-BVhu_VZKm3L2cAQn6oHFxJrTlhZZ0mJDZ2J0I,2864
4
+ ytdl/downloader.py,sha256=ebZpMMqHraruKAT2LFBzdkMJ0HpBx6s9JvqnF6_T4PQ,4607
5
+ ytdl/profiler.py,sha256=qekkm6SFob4e26pxpsG4SmLNqs7thTQNGRpfeK87IH4,2081
6
+ ytdl/utils.py,sha256=-2Y_89n_RhTWfACAVR-_Dtyjpdq-5Px5J5xii-rPGt4,1188
7
+ udown-0.0.2.dist-info/METADATA,sha256=yh7e48w7gFO2YWzuWVTynTd6DjVLpz5A5zG9O4zbKZY,6442
8
+ udown-0.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ udown-0.0.2.dist-info/entry_points.txt,sha256=JogR0ArrJoCDi1JC-U1SzjWmVX-8cAqHKfrRBfNxiAU,40
10
+ udown-0.0.2.dist-info/top_level.txt,sha256=S8I8RsIAG6qsXrOdAVauYxr2PGVaG-k_0tAnY6gEEVA,5
11
+ udown-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ udown = ytdl.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dipanjal Maitra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ytdl
ytdl/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ YouTube Downloader with Parallel Processing
3
+ A fast YouTube video downloader that downloads audio and video streams in parallel.
4
+ """
5
+
6
+ from .downloader import Downloader
7
+ from .profiler import Profiler
8
+ from .utils import Utils
9
+
10
+ __version__ = "1.0.0"
11
+ __all__ = ["Downloader", "Profiler", "Utils"]
ytdl/cli.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ YouTube Downloader CLI
3
+ A command-line interface for downloading YouTube videos with parallel processing.
4
+ """
5
+
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ytdl.downloader import Downloader
11
+
12
+
13
+ def create_parser() -> argparse.ArgumentParser:
14
+ """Create and configure the argument parser."""
15
+ parser = argparse.ArgumentParser(
16
+ description="Download YouTube videos with parallel processing",
17
+ formatter_class=argparse.RawDescriptionHelpFormatter,
18
+ epilog="""
19
+ Examples:
20
+ %(prog)s https://www.youtube.com/watch?v=dQw4w9WgXcQ
21
+ %(prog)s -o /path/to/downloads https://www.youtube.com/watch?v=dQw4w9WgXcQ
22
+ %(prog)s -c -d https://www.youtube.com/watch?v=dQw4w9WgXcQ
23
+ """,
24
+ )
25
+ parser.add_argument("url", help="YouTube video URL to download")
26
+ parser.add_argument(
27
+ "-o",
28
+ "--output",
29
+ dest="output_dir",
30
+ help="Output directory for downloaded files (default: ./downloads)",
31
+ )
32
+ parser.add_argument(
33
+ "-c",
34
+ "--caption",
35
+ action="store_true",
36
+ help="Download captions/subtitles if available",
37
+ )
38
+ parser.add_argument(
39
+ "-d",
40
+ "--debug",
41
+ action="store_true",
42
+ help="Enable debug mode with detailed timing information",
43
+ )
44
+ parser.add_argument("--version", action="version", version="%(prog)s 1.0.0")
45
+ return parser
46
+
47
+
48
+ def validate_url(url: str) -> bool:
49
+ """Validate if the URL is a valid YouTube URL."""
50
+ youtube_domains = ["youtube.com", "www.youtube.com", "youtu.be", "m.youtube.com"]
51
+
52
+ url_lower = url.lower()
53
+ return any(domain in url_lower for domain in youtube_domains)
54
+
55
+
56
+ def main() -> None:
57
+ """Main CLI entry point."""
58
+ parser = create_parser()
59
+ args = parser.parse_args()
60
+
61
+ # Validate URL
62
+ if not validate_url(args.url):
63
+ print("Error: Please provide a valid YouTube URL")
64
+ print("Supported formats:")
65
+ print(" - https://www.youtube.com/watch?v=VIDEO_ID")
66
+ print(" - https://youtu.be/VIDEO_ID")
67
+ print(" - https://m.youtube.com/watch?v=VIDEO_ID")
68
+ sys.exit(1)
69
+
70
+ # Set output directory
71
+ output_dir = args.output_dir if args.output_dir else "./downloads"
72
+
73
+ # Create output directory if it doesn't exist
74
+ output_path = Path(output_dir)
75
+ output_path.mkdir(parents=True, exist_ok=True)
76
+
77
+ try:
78
+ # Initialize downloader
79
+ downloader = Downloader(
80
+ url=args.url,
81
+ out_dir=str(output_path),
82
+ caption=args.caption,
83
+ debug=args.debug,
84
+ )
85
+
86
+ # Start download
87
+ downloader.start()
88
+
89
+ except KeyboardInterrupt:
90
+ print("\nDownload cancelled by user")
91
+ sys.exit(1)
92
+ except Exception as e:
93
+ print(f"Error: {str(e)}")
94
+ sys.exit(1)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
ytdl/downloader.py ADDED
@@ -0,0 +1,126 @@
1
+ import concurrent.futures
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from pytubefix import YouTube # type: ignore
6
+ from pytubefix.cli import on_progress # type: ignore
7
+
8
+ from ytdl.profiler import Profiler
9
+ from ytdl.utils import Utils
10
+
11
+ ROOT_DIR = Path(__file__).parent.parent.resolve()
12
+ DOWNLOAD_DIR = ROOT_DIR / "downloads"
13
+ TEMP_DIR = ROOT_DIR / "temp"
14
+
15
+
16
+ class Downloader:
17
+ def __init__(
18
+ self,
19
+ url: str,
20
+ out_dir: str = str(DOWNLOAD_DIR),
21
+ caption: bool = False,
22
+ debug: bool = False,
23
+ ):
24
+ self.url = url
25
+ self.out_dir = out_dir
26
+ self.caption = caption
27
+ self.debug = debug
28
+ self._pre_config()
29
+
30
+ def _pre_config(self) -> None:
31
+ self.yt = YouTube(self.url, on_progress_callback=on_progress)
32
+ self.title = Utils.sanitize_filename(self.yt.title)
33
+ self.profiler = Profiler(self.debug)
34
+
35
+ # Setting up temp files
36
+ self.temp_video_file: Path = TEMP_DIR / "temp.mp4"
37
+ self.temp_audio_file: Path = TEMP_DIR / "temp.m4a"
38
+
39
+ # Create out directory if not exists
40
+ TEMP_DIR.mkdir(exist_ok=True)
41
+ DOWNLOAD_DIR.mkdir(exist_ok=True)
42
+ if self.out_dir != str(DOWNLOAD_DIR):
43
+ Path(self.out_dir).mkdir(exist_ok=True)
44
+
45
+ def _cleanup_temps(self) -> None:
46
+ # Clean up temporary files
47
+ Utils.delete_file(self.temp_video_file)
48
+ Utils.delete_file(self.temp_audio_file)
49
+
50
+ def _merge_with_ffmpeg(self) -> str:
51
+ self.profiler.start_timer("merging")
52
+ out_file_path: str = os.path.join(self.out_dir, f"{self.title}.mp4")
53
+ Utils.merge_with_ffmpeg(
54
+ video_file=str(self.temp_video_file),
55
+ audio_file=str(self.temp_audio_file),
56
+ out_file=out_file_path,
57
+ )
58
+ self.profiler.end_timer("merging")
59
+ return out_file_path
60
+
61
+ def _download_audio_file(self, file_path: Path) -> None:
62
+ """Download the best audio stream."""
63
+ print("Downloading Audio File")
64
+ self.profiler.start_timer("audio")
65
+ self.yt.streams.filter(only_audio=True).order_by("abr").desc().first().download(
66
+ output_path=str(file_path.parent), filename=file_path.name
67
+ )
68
+ self.profiler.end_timer("audio")
69
+
70
+ def _download_video_file(self, file_path: Path) -> None:
71
+ """Download the best video stream."""
72
+ print("Downloading Video File")
73
+ self.profiler.start_timer("video")
74
+ self.yt.streams.filter(only_video=True, file_extension="mp4").order_by("resolution").desc().first().download(
75
+ output_path=str(file_path.parent), filename=file_path.name
76
+ )
77
+ self.profiler.end_timer("video")
78
+
79
+ def _download_caption_file(self) -> None:
80
+ caption = self.yt.captions.get("a.en", None)
81
+ if not caption:
82
+ print("No caption found")
83
+ return
84
+
85
+ out_file_path: str = os.path.join(self.out_dir, f"{self.title}.srt")
86
+ try:
87
+ self.profiler.start_timer("caption")
88
+ print("Downloading Caption File")
89
+ caption.save_captions(out_file_path)
90
+ self.profiler.end_timer("caption")
91
+ except Exception as e:
92
+ print("Unable to download caption file: ", str(e))
93
+ Utils.delete_file(out_file_path)
94
+
95
+ def _download(self) -> None:
96
+ with concurrent.futures.ThreadPoolExecutor() as executor:
97
+ # Submit video and audio download tasks
98
+ future_video = executor.submit(self._download_video_file, self.temp_video_file)
99
+ future_audio = executor.submit(self._download_audio_file, self.temp_audio_file)
100
+
101
+ # Submit caption download task if caption is requested
102
+ futures = [future_video, future_audio]
103
+ if self.caption:
104
+ future_caption = executor.submit(self._download_caption_file)
105
+ futures.append(future_caption)
106
+
107
+ # Wait for all downloads to complete
108
+ concurrent.futures.wait(futures)
109
+
110
+ def start(self) -> None:
111
+ """Download video and audio, then merge them with FFmpeg."""
112
+ print(f"Downloading: {self.title}")
113
+ self.profiler.start_overall_timer()
114
+
115
+ try:
116
+ self._cleanup_temps()
117
+ self._download()
118
+ out_file_path: str = self._merge_with_ffmpeg()
119
+ print(f"Video downloaded successfully to: {out_file_path}")
120
+ except Exception as e:
121
+ print(f"Video downloaded failed due to unknown error: {str(e)}")
122
+ finally:
123
+ self._cleanup_temps()
124
+
125
+ self.profiler.end_overall_timer()
126
+ self.profiler.print_report()
ytdl/profiler.py ADDED
@@ -0,0 +1,56 @@
1
+ import time
2
+ from typing import Optional
3
+
4
+
5
+ class Profiler:
6
+ def __init__(self, debug: bool = False):
7
+ self.timings: dict[str, dict[str, float]] = {}
8
+ self.start_time: Optional[float] = None
9
+ self.debug = debug
10
+
11
+ def start_timer(self, name: str) -> None:
12
+ """Start timing a specific operation."""
13
+ self.timings[name] = {"start": time.time()}
14
+
15
+ def end_timer(self, name: str) -> None:
16
+ """End timing a specific operation and calculate duration."""
17
+ if name in self.timings:
18
+ self.timings[name]["end"] = time.time()
19
+ self.timings[name]["duration"] = self.timings[name]["end"] - self.timings[name]["start"]
20
+
21
+ def start_overall_timer(self) -> None:
22
+ """Start the overall process timer."""
23
+ self.start_time = time.time()
24
+
25
+ def end_overall_timer(self) -> None:
26
+ """End the overall process timer and calculate total duration."""
27
+ if self.start_time:
28
+ total_duration = time.time() - self.start_time
29
+ self.timings["total"] = {"duration": total_duration}
30
+
31
+ def print_report(self) -> None:
32
+ """Print a formatted timing report."""
33
+ sum_of_duration: float = 0.0
34
+ if self.debug:
35
+ print("\n" + "=" * 50)
36
+ print("DOWNLOAD PERFORMANCE REPORT")
37
+ print("=" * 50)
38
+
39
+ for name, timing in self.timings.items():
40
+ if name != "total":
41
+ duration = timing.get("duration", 0)
42
+ sum_of_duration += duration
43
+ print(f"{name.capitalize()} Took: {duration:.2f} seconds")
44
+
45
+ print("=" * 50)
46
+
47
+ if "total" in self.timings:
48
+ if self.debug:
49
+ print(f"Expected Duration: {sum_of_duration:.2f} seconds")
50
+
51
+ actual_total_duration = self.timings["total"]["duration"]
52
+ print(f"Total Process Time: {actual_total_duration:.2f} seconds")
53
+
54
+ if self.debug:
55
+ print("=" * 50)
56
+ print(f"Time Saved: {(sum_of_duration - actual_total_duration):.2f} seconds")
ytdl/utils.py ADDED
@@ -0,0 +1,46 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Union
6
+
7
+
8
+ class Utils:
9
+ @staticmethod
10
+ def sanitize_filename(name: str) -> str:
11
+ """Sanitize a string to be a valid filename."""
12
+ return re.sub(r'[\\/*?:"<>|]', "", name)
13
+
14
+ @staticmethod
15
+ def delete_file(file_path: Union[Path, str]) -> None:
16
+ if isinstance(file_path, str):
17
+ file_path = Path(file_path)
18
+
19
+ if file_path.exists():
20
+ os.remove(file_path)
21
+
22
+ @staticmethod
23
+ def merge_with_ffmpeg(video_file: str, audio_file: str, out_file: str, debug: bool = False) -> None:
24
+ """
25
+ Merge video and audio using ffmpeg via subprocess.
26
+ """
27
+ cmd = [
28
+ "ffmpeg",
29
+ "-y", # Overwrite output file if it exists
30
+ "-i",
31
+ video_file,
32
+ "-i",
33
+ audio_file,
34
+ "-c:v",
35
+ "copy",
36
+ "-c:a",
37
+ "aac",
38
+ "-map",
39
+ "0:v:0",
40
+ "-map",
41
+ "1:a:0",
42
+ out_file,
43
+ ]
44
+ if debug:
45
+ print("Running ffmpeg command:", " ".join(cmd))
46
+ subprocess.run(cmd, check=True)