idm-cli 1.3.0__tar.gz → 1.4.0__tar.gz

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.
Files changed (39) hide show
  1. idm_cli-1.4.0/MANIFEST.in +1 -0
  2. {idm_cli-1.3.0/idm_cli.egg-info → idm_cli-1.4.0}/PKG-INFO +28 -88
  3. {idm_cli-1.3.0 → idm_cli-1.4.0}/README.md +21 -78
  4. idm_cli-1.4.0/idm_cli/__init__.py +3 -0
  5. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/config.py +17 -7
  6. idm_cli-1.4.0/idm_cli/downloader/converter.py +147 -0
  7. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/core.py +33 -93
  8. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/downloader.py +60 -23
  9. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/handlers.py +58 -15
  10. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/muxer.py +244 -179
  11. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/state.py +5 -3
  12. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/__init__.py +18 -18
  13. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/winget.py +5 -1
  14. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/ui/cli.py +30 -19
  15. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/ui/utils.py +12 -4
  16. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/update_checker.py +5 -2
  17. {idm_cli-1.3.0 → idm_cli-1.4.0/idm_cli.egg-info}/PKG-INFO +28 -88
  18. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/SOURCES.txt +5 -5
  19. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/requires.txt +3 -0
  20. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/top_level.txt +1 -0
  21. idm_cli-1.4.0/pyproject.toml +45 -0
  22. {idm_cli-1.3.0 → idm_cli-1.4.0}/requirements.txt +0 -2
  23. {idm_cli-1.3.0 → idm_cli-1.4.0}/setup.py +1 -3
  24. idm_cli-1.4.0/tests/test_core.py +148 -0
  25. idm_cli-1.3.0/MANIFEST.in +0 -2
  26. idm_cli-1.3.0/idm_cli/__init__.py +0 -3
  27. idm_cli-1.3.0/idm_cli/extension/daemon.py +0 -44
  28. idm_cli-1.3.0/idm_cli/extension/extension.py +0 -161
  29. idm_cli-1.3.0/idm_cli/extension/native_host.py +0 -124
  30. {idm_cli-1.3.0 → idm_cli-1.4.0}/LICENSE +0 -0
  31. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/__main__.py +0 -0
  32. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/__init__.py +0 -0
  33. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/direct.py +0 -0
  34. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/ytdlp.py +0 -0
  35. {idm_cli-1.3.0/idm_cli/extension → idm_cli-1.4.0/idm_cli/ui}/__init__.py +0 -0
  36. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/dependency_links.txt +0 -0
  37. {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/entry_points.txt +0 -0
  38. {idm_cli-1.3.0 → idm_cli-1.4.0}/setup.cfg +0 -0
  39. {idm_cli-1.3.0/idm_cli/ui → idm_cli-1.4.0/tests}/__init__.py +0 -0
@@ -0,0 +1 @@
1
+ include requirements.txt
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idm-cli
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: A lightning-fast, universal command-line download manager
5
5
  Home-page: https://github.com/rj41-w2/idm-cli
6
6
  Author: Rehan
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/rj41-w2/idm-cli
7
9
  Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
10
  Classifier: Operating System :: OS Independent
10
11
  Classifier: Environment :: Console
11
12
  Requires-Python: >=3.8
@@ -22,35 +23,43 @@ Requires-Dist: aiodns
22
23
  Requires-Dist: Brotli
23
24
  Requires-Dist: psutil
24
25
  Requires-Dist: platformdirs
25
- Requires-Dist: pytest
26
- Requires-Dist: pytest-asyncio
27
- Dynamic: author
28
- Dynamic: classifier
29
- Dynamic: description
30
- Dynamic: description-content-type
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Requires-Dist: pytest-asyncio; extra == "dev"
29
+ Requires-Dist: packaging; extra == "dev"
31
30
  Dynamic: home-page
32
31
  Dynamic: license-file
33
- Dynamic: requires-dist
34
32
  Dynamic: requires-python
35
- Dynamic: summary
36
33
 
37
- # IDM-CLI (Internet Download Manager CLI)
34
+ <div align="center">
38
35
 
39
- *Inspired by the official [Internet Download Manager (IDM)](https://www.internetdownloadmanager.com/)*
36
+ <h1>IDM-CLI</h1>
40
37
 
41
- A lightning-fast, cross-platform command-line download manager written in Python. IDM-CLI splits files into multiple parallel chunks (default 8, up to 32) to maximize your internet speed. It supports downloading from **YouTube**, **Facebook**, **Instagram**, and any **Direct File URL** (`.exe`, `.zip`, `.pdf`, etc.).
38
+ **A lightning-fast, cross-platform command-line download manager written in Python.**
39
+
40
+ *The official IDM asks you to pay after 30 days — IDM-CLI is 100% free, open-source, and yours to modify!*
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/idm-cli?color=blue&label=pypi%20version)](https://pypi.org/project/idm-cli/)
43
+ [![Downloads](https://static.pepy.tech/badge/idm-cli)](https://pepy.tech/project/idm-cli)
44
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/rj41-w2/idm-cli/blob/main/LICENSE)
45
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
46
+ [![Install](https://img.shields.io/badge/install-pip%20install%20idm--cli-brightgreen)](https://pypi.org/project/idm-cli/)
47
+
48
+ [Download via PyPI](https://pypi.org/project/idm-cli/) · [Report a Bug](https://github.com/rj41-w2/idm-cli/issues)
49
+
50
+ </div>
51
+
52
+ IDM-CLI splits files into multiple parallel chunks (default 8, up to 32) to maximize your internet speed. It supports downloading from **YouTube**, **Facebook**, **Instagram**, and any **Direct File URL** (`.exe`, `.zip`, `.pdf`, etc.).
42
53
 
43
54
  ## Features
44
55
 
45
56
  - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `aiohttp` async sessions to fully saturate your bandwidth.
46
- - **Universal Downloader** — Paste any direct URL. A built-in HTTP `HEAD` detector automatically recognizes file types and routes them to the parallel engine.
47
- - **Social Media Support** — Natively supports YouTube, Facebook, and Instagram via a modular extractor architecture with yt-dlp.
57
+ - **Universal Downloader** — Paste any URL. yt-dlp handles 1000+ sites (YouTube, Facebook, Instagram, etc.), with automatic fallback to direct HTTP download for unsupported URLs.
48
58
  - **Audio Only / MP3 Conversion** — Download videos as audio. IDM-CLI automatically grabs the best audio stream and uses FFmpeg to convert it to `.mp3`.
49
59
  - **Smart Auto-Resume** — Internet dropped? Pressed `Ctrl+C`? No problem. IDM-CLI remembers the exact byte positions of all incomplete chunks. Paste the same link again to resume instantly.
50
60
  - **Persistent Queue System** — Add multiple files to a queue with `-Q` and type `start queue` to download them all sequentially.
51
- - **Browser Extension Integration** — Chrome/Edge extension intercepts downloads and routes them directly into IDM-CLI via Native Messaging.
52
61
  - **Interactive UI** — Modern terminal interface powered by `rich` and `questionary` with real-time speed, ETA, and progress bars. Press `p` to pause, `r` to resume.
53
- - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling and browser registration.
62
+ - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling.
54
63
  - **Auto-Update Check** — Checks PyPI once a day and notifies you when a new version is available.
55
64
 
56
65
  ## Prerequisites
@@ -118,22 +127,9 @@ idm "https://facebook.com/..." -Q
118
127
  | `<URL>` | Download a video or file |
119
128
  | `resume` | Resume or delete incomplete downloads |
120
129
  | `start queue` | Process all queued downloads |
121
- | `install extension` | Guide to install the browser extension |
122
130
  | `help` | Show available commands and flags |
123
131
  | `exit` | Exit the application |
124
132
 
125
- ### Browser Extension
126
-
127
- IDM-CLI includes a Chrome/Edge extension that intercepts file downloads automatically. To install it:
128
-
129
- ```bash
130
- idm
131
- # then type:
132
- install extension
133
- ```
134
-
135
- Follow the on-screen guide to load the unpacked extension and link it to IDM-CLI.
136
-
137
133
  ### Troubleshooting: Windows Device Guard
138
134
 
139
135
  If you get `"idm.exe was blocked by your organization's Device Guard policy"`, run the Python module directly:
@@ -161,69 +157,13 @@ idm_cli/
161
157
  │ ├── ytdlp.py # YouTube/social media extractor
162
158
  │ ├── direct.py # Direct HTTP file downloader
163
159
  │ └── winget.py # Windows Package Manager extractor
164
- └── extension/
165
- ├── extension.py # Browser extension installer
166
- ├── native_host.py # Native Messaging Host
167
- ├── daemon.py # Queue daemon with PID lock
168
- └── browser_extension/ # Chrome/Edge extension source
169
160
  ```
170
161
 
171
162
  ## Contributing
172
163
 
173
- Contributions are welcome — whether it's a bug fix, a new feature, or just improving the docs.
174
-
175
- ### Reporting Bugs
176
-
177
- If you find a bug, [open an issue](https://github.com/rj41-w2/idm-cli/issues/new) with:
178
-
179
- - A clear title and description of the problem
180
- - Steps to reproduce the issue
181
- - Your OS and Python version (`python --version`)
182
- - The full error output if there is one
183
-
184
- ### Suggesting Features
185
-
186
- Got an idea? [Open an issue](https://github.com/rj41-w2/idm-cli/issues/new) with the tag `enhancement` and describe:
187
-
188
- - What you want to happen and why
189
- - How it would work from the user's perspective
190
- - Any alternatives you considered
191
-
192
- ### Submitting a Pull Request
193
-
194
- 1. **Fork** the repository from [github.com/rj41-w2/idm-cli](https://github.com/rj41-w2/idm-cli)
195
-
196
- 2. **Clone your fork** and set it up:
197
- ```bash
198
- git clone https://github.com/<your-username>/idm-cli.git
199
- cd IDM-CLI
200
- pip install -r requirements.txt
201
- pip install -e .
202
- ```
203
-
204
- 3. **Create a branch** for your change:
205
- ```bash
206
- git checkout -b fix/short-description
207
- ```
208
-
209
- 4. **Make your changes**, test them, and commit with a clear message:
210
- ```bash
211
- git commit -m "fix: describe the bug you fixed"
212
- ```
213
-
214
- 5. **Push** to your fork and open a Pull Request against `main`:
215
- ```bash
216
- git push origin fix/short-description
217
- ```
218
-
219
- 6. In your PR description, explain **what changed** and **why**. Link the issue if there is one.
220
-
221
- ### Guidelines
164
+ Contributions are welcome. Fork the repo, make your changes on a feature branch, and open a PR against `main`. Keep changes focused and follow the existing code style.
222
165
 
223
- - Keep PRs focused on a single change. Don't mix unrelated fixes.
224
- - Follow the existing code style — no comments unless absolutely necessary.
225
- - Test your changes on at least one platform before submitting.
226
- - If you're adding a feature, update the README and spec if needed.
166
+ For bug reports and feature requests, [open an issue](https://github.com/rj41-w2/idm-cli/issues).
227
167
 
228
168
  ## License
229
169
 
@@ -1,20 +1,32 @@
1
- # IDM-CLI (Internet Download Manager CLI)
1
+ <div align="center">
2
2
 
3
- *Inspired by the official [Internet Download Manager (IDM)](https://www.internetdownloadmanager.com/)*
3
+ <h1>IDM-CLI</h1>
4
4
 
5
- A lightning-fast, cross-platform command-line download manager written in Python. IDM-CLI splits files into multiple parallel chunks (default 8, up to 32) to maximize your internet speed. It supports downloading from **YouTube**, **Facebook**, **Instagram**, and any **Direct File URL** (`.exe`, `.zip`, `.pdf`, etc.).
5
+ **A lightning-fast, cross-platform command-line download manager written in Python.**
6
+
7
+ *The official IDM asks you to pay after 30 days — IDM-CLI is 100% free, open-source, and yours to modify!*
8
+
9
+ [![PyPI version](https://img.shields.io/pypi/v/idm-cli?color=blue&label=pypi%20version)](https://pypi.org/project/idm-cli/)
10
+ [![Downloads](https://static.pepy.tech/badge/idm-cli)](https://pepy.tech/project/idm-cli)
11
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/rj41-w2/idm-cli/blob/main/LICENSE)
12
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
13
+ [![Install](https://img.shields.io/badge/install-pip%20install%20idm--cli-brightgreen)](https://pypi.org/project/idm-cli/)
14
+
15
+ [Download via PyPI](https://pypi.org/project/idm-cli/) · [Report a Bug](https://github.com/rj41-w2/idm-cli/issues)
16
+
17
+ </div>
18
+
19
+ IDM-CLI splits files into multiple parallel chunks (default 8, up to 32) to maximize your internet speed. It supports downloading from **YouTube**, **Facebook**, **Instagram**, and any **Direct File URL** (`.exe`, `.zip`, `.pdf`, etc.).
6
20
 
7
21
  ## Features
8
22
 
9
23
  - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `aiohttp` async sessions to fully saturate your bandwidth.
10
- - **Universal Downloader** — Paste any direct URL. A built-in HTTP `HEAD` detector automatically recognizes file types and routes them to the parallel engine.
11
- - **Social Media Support** — Natively supports YouTube, Facebook, and Instagram via a modular extractor architecture with yt-dlp.
24
+ - **Universal Downloader** — Paste any URL. yt-dlp handles 1000+ sites (YouTube, Facebook, Instagram, etc.), with automatic fallback to direct HTTP download for unsupported URLs.
12
25
  - **Audio Only / MP3 Conversion** — Download videos as audio. IDM-CLI automatically grabs the best audio stream and uses FFmpeg to convert it to `.mp3`.
13
26
  - **Smart Auto-Resume** — Internet dropped? Pressed `Ctrl+C`? No problem. IDM-CLI remembers the exact byte positions of all incomplete chunks. Paste the same link again to resume instantly.
14
27
  - **Persistent Queue System** — Add multiple files to a queue with `-Q` and type `start queue` to download them all sequentially.
15
- - **Browser Extension Integration** — Chrome/Edge extension intercepts downloads and routes them directly into IDM-CLI via Native Messaging.
16
28
  - **Interactive UI** — Modern terminal interface powered by `rich` and `questionary` with real-time speed, ETA, and progress bars. Press `p` to pause, `r` to resume.
17
- - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling and browser registration.
29
+ - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling.
18
30
  - **Auto-Update Check** — Checks PyPI once a day and notifies you when a new version is available.
19
31
 
20
32
  ## Prerequisites
@@ -82,22 +94,9 @@ idm "https://facebook.com/..." -Q
82
94
  | `<URL>` | Download a video or file |
83
95
  | `resume` | Resume or delete incomplete downloads |
84
96
  | `start queue` | Process all queued downloads |
85
- | `install extension` | Guide to install the browser extension |
86
97
  | `help` | Show available commands and flags |
87
98
  | `exit` | Exit the application |
88
99
 
89
- ### Browser Extension
90
-
91
- IDM-CLI includes a Chrome/Edge extension that intercepts file downloads automatically. To install it:
92
-
93
- ```bash
94
- idm
95
- # then type:
96
- install extension
97
- ```
98
-
99
- Follow the on-screen guide to load the unpacked extension and link it to IDM-CLI.
100
-
101
100
  ### Troubleshooting: Windows Device Guard
102
101
 
103
102
  If you get `"idm.exe was blocked by your organization's Device Guard policy"`, run the Python module directly:
@@ -125,69 +124,13 @@ idm_cli/
125
124
  │ ├── ytdlp.py # YouTube/social media extractor
126
125
  │ ├── direct.py # Direct HTTP file downloader
127
126
  │ └── winget.py # Windows Package Manager extractor
128
- └── extension/
129
- ├── extension.py # Browser extension installer
130
- ├── native_host.py # Native Messaging Host
131
- ├── daemon.py # Queue daemon with PID lock
132
- └── browser_extension/ # Chrome/Edge extension source
133
127
  ```
134
128
 
135
129
  ## Contributing
136
130
 
137
- Contributions are welcome — whether it's a bug fix, a new feature, or just improving the docs.
138
-
139
- ### Reporting Bugs
140
-
141
- If you find a bug, [open an issue](https://github.com/rj41-w2/idm-cli/issues/new) with:
142
-
143
- - A clear title and description of the problem
144
- - Steps to reproduce the issue
145
- - Your OS and Python version (`python --version`)
146
- - The full error output if there is one
147
-
148
- ### Suggesting Features
149
-
150
- Got an idea? [Open an issue](https://github.com/rj41-w2/idm-cli/issues/new) with the tag `enhancement` and describe:
151
-
152
- - What you want to happen and why
153
- - How it would work from the user's perspective
154
- - Any alternatives you considered
155
-
156
- ### Submitting a Pull Request
157
-
158
- 1. **Fork** the repository from [github.com/rj41-w2/idm-cli](https://github.com/rj41-w2/idm-cli)
159
-
160
- 2. **Clone your fork** and set it up:
161
- ```bash
162
- git clone https://github.com/<your-username>/idm-cli.git
163
- cd IDM-CLI
164
- pip install -r requirements.txt
165
- pip install -e .
166
- ```
167
-
168
- 3. **Create a branch** for your change:
169
- ```bash
170
- git checkout -b fix/short-description
171
- ```
172
-
173
- 4. **Make your changes**, test them, and commit with a clear message:
174
- ```bash
175
- git commit -m "fix: describe the bug you fixed"
176
- ```
177
-
178
- 5. **Push** to your fork and open a Pull Request against `main`:
179
- ```bash
180
- git push origin fix/short-description
181
- ```
182
-
183
- 6. In your PR description, explain **what changed** and **why**. Link the issue if there is one.
184
-
185
- ### Guidelines
131
+ Contributions are welcome. Fork the repo, make your changes on a feature branch, and open a PR against `main`. Keep changes focused and follow the existing code style.
186
132
 
187
- - Keep PRs focused on a single change. Don't mix unrelated fixes.
188
- - Follow the existing code style — no comments unless absolutely necessary.
189
- - Test your changes on at least one platform before submitting.
190
- - If you're adding a feature, update the README and spec if needed.
133
+ For bug reports and feature requests, [open an issue](https://github.com/rj41-w2/idm-cli/issues).
191
134
 
192
135
  ## License
193
136
 
@@ -0,0 +1,3 @@
1
+ # IDM-CLI Package
2
+
3
+ __version__ = "1.4.0"
@@ -6,13 +6,15 @@ import logging
6
6
  from logging.handlers import RotatingFileHandler
7
7
  from platformdirs import user_data_dir, user_downloads_dir
8
8
 
9
+ __all__ = ["CONFIG_DIR", "CONFIG_FILE", "logger", "load_config", "save_config", "setup_logging"]
10
+
9
11
  APP_NAME = "idm-cli"
10
12
  APP_AUTHOR = "idm-cli"
11
13
  CONFIG_DIR = user_data_dir(APP_NAME, APP_AUTHOR)
12
14
  CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
13
15
  LOG_FILE = os.path.join(CONFIG_DIR, "idm.log")
14
16
 
15
- def _migrate_old_config():
17
+ def _migrate_old_config() -> None:
16
18
  old_dir = os.path.expanduser("~/.idm_cli")
17
19
  if old_dir == CONFIG_DIR:
18
20
  return
@@ -20,6 +22,8 @@ def _migrate_old_config():
20
22
  return
21
23
  os.makedirs(CONFIG_DIR, exist_ok=True)
22
24
  for item in os.listdir(old_dir):
25
+ if item == "__pycache__":
26
+ continue
23
27
  src = os.path.join(old_dir, item)
24
28
  dst = os.path.join(CONFIG_DIR, item)
25
29
  if not os.path.exists(dst):
@@ -34,16 +38,22 @@ def _migrate_old_config():
34
38
  def _get_default_download_dir() -> str:
35
39
  return user_downloads_dir()
36
40
 
37
- def setup_logging():
41
+ def setup_logging() -> logging.Logger:
38
42
  _migrate_old_config()
39
43
  os.makedirs(CONFIG_DIR, exist_ok=True)
40
44
  logger = logging.getLogger("idm_cli")
41
45
  logger.setLevel(logging.DEBUG)
42
46
  if not logger.handlers:
43
- handler = RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=2)
44
47
  formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
45
- handler.setFormatter(formatter)
46
- logger.addHandler(handler)
48
+
49
+ file_handler = RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=2, encoding="utf-8")
50
+ file_handler.setFormatter(formatter)
51
+ logger.addHandler(file_handler)
52
+
53
+ stream_handler = logging.StreamHandler()
54
+ stream_handler.setFormatter(formatter)
55
+ stream_handler.stream.reconfigure(errors="backslashreplace")
56
+ logger.addHandler(stream_handler)
47
57
  return logger
48
58
 
49
59
  logger = setup_logging()
@@ -55,7 +65,7 @@ DEFAULT_CONFIG = {
55
65
  "last_version": "0.0.0"
56
66
  }
57
67
 
58
- def load_config():
68
+ def load_config() -> dict:
59
69
  config = DEFAULT_CONFIG.copy()
60
70
  if os.path.exists(CONFIG_FILE):
61
71
  try:
@@ -66,7 +76,7 @@ def load_config():
66
76
  logger.warning(f"Failed to load config: {e}")
67
77
  return config
68
78
 
69
- def save_config(config_data):
79
+ def save_config(config_data: dict) -> None:
70
80
  os.makedirs(CONFIG_DIR, exist_ok=True)
71
81
  try:
72
82
  with open(CONFIG_FILE, "w") as f:
@@ -0,0 +1,147 @@
1
+ import os
2
+ import logging
3
+ import subprocess
4
+ from typing import Optional
5
+
6
+ from rich.table import Table
7
+
8
+ from idm_cli.ui.utils import console
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v", ".ts", ".mpg", ".mpeg", ".3gp"}
13
+
14
+
15
+ def resolve_input_path(path_str: str) -> str:
16
+ cleaned = path_str.strip()
17
+ if len(cleaned) >= 2 and cleaned[0] == cleaned[-1] and cleaned[0] in ("'", '"'):
18
+ cleaned = cleaned[1:-1].strip()
19
+ return os.path.abspath(os.path.expandvars(os.path.expanduser(cleaned)))
20
+
21
+
22
+ def is_local_path(path_str: str) -> bool:
23
+ try:
24
+ resolved = resolve_input_path(path_str)
25
+ return os.path.isfile(resolved) or os.path.isdir(resolved)
26
+ except (OSError, ValueError):
27
+ return False
28
+
29
+
30
+ def collect_video_files(path_str: str) -> list:
31
+ resolved = resolve_input_path(path_str)
32
+ if os.path.isdir(resolved):
33
+ videos = []
34
+ for name in sorted(os.listdir(resolved)):
35
+ full_path = os.path.join(resolved, name)
36
+ if os.path.isfile(full_path) and os.path.splitext(name)[1].lower() in VIDEO_EXTENSIONS:
37
+ videos.append(full_path)
38
+ if not videos:
39
+ raise ValueError(f"No video files found in folder: {resolved}")
40
+ return videos
41
+ if os.path.isfile(resolved):
42
+ if os.path.splitext(resolved)[1].lower() in VIDEO_EXTENSIONS:
43
+ return [resolved]
44
+ supported = ", ".join(sorted(ext.lstrip(".") for ext in VIDEO_EXTENSIONS))
45
+ raise ValueError(f"Not a supported video format: {resolved} (Supported: {supported})")
46
+ raise ValueError(f"Path not found: {resolved}")
47
+
48
+
49
+ def convert_video_to_audio(video_path: str, output_path: Optional[str] = None) -> str:
50
+ """
51
+ Converts a local video file to MP3 using ffmpeg.
52
+ Never deletes the source video.
53
+ """
54
+ if not os.path.exists(video_path):
55
+ raise FileNotFoundError(f"Video file not found: {video_path}")
56
+
57
+ from idm_cli.downloader.muxer import get_ffmpeg_path, _ffmpeg_install_hint
58
+
59
+ ffmpeg_bin = get_ffmpeg_path()
60
+ if not ffmpeg_bin:
61
+ raise RuntimeError("FFmpeg is not installed! Cannot convert video.")
62
+
63
+ if output_path is None:
64
+ base, _ = os.path.splitext(video_path)
65
+ output_path = base + ".mp3"
66
+
67
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
68
+
69
+ cmd = [
70
+ ffmpeg_bin,
71
+ "-y",
72
+ "-i", video_path,
73
+ "-vn",
74
+ "-q:a", "0",
75
+ output_path
76
+ ]
77
+
78
+ try:
79
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
80
+ except subprocess.CalledProcessError as e:
81
+ logger.error(f"FFmpeg conversion failed: {e.stderr.decode('utf-8', errors='replace')}")
82
+ raise RuntimeError(f"FFmpeg conversion failed: {e}")
83
+ except FileNotFoundError:
84
+ raise RuntimeError(f"FFmpeg is not installed! {_ffmpeg_install_hint()}")
85
+
86
+ return output_path
87
+
88
+
89
+ def process_conversion(path_str: str, delete_source: bool = False, is_interactive: bool = True) -> bool:
90
+ from idm_cli.downloader.muxer import get_ffmpeg_path, ensure_ffmpeg
91
+
92
+ try:
93
+ videos = collect_video_files(path_str)
94
+ except ValueError as e:
95
+ console.print(f"[bold red]{e}")
96
+ return False
97
+
98
+ if not get_ffmpeg_path() and not ensure_ffmpeg(is_interactive=is_interactive):
99
+ return False
100
+
101
+ total = len(videos)
102
+ converted_count = 0
103
+ results = []
104
+
105
+ if total > 1:
106
+ console.print(f"\n[bold cyan]Found {total} videos. Converting to MP3...[/bold cyan]\n")
107
+
108
+ try:
109
+ for index, video in enumerate(videos, start=1):
110
+ label = os.path.basename(video)
111
+ prefix = f"({index}/{total}) " if total > 1 else ""
112
+ try:
113
+ with console.status(f"[bold cyan]{prefix}Converting {label} to MP3...", spinner="dots"):
114
+ output_path = convert_video_to_audio(video)
115
+ results.append((label, True, output_path))
116
+ converted_count += 1
117
+ if delete_source:
118
+ try:
119
+ os.remove(video)
120
+ except OSError as e:
121
+ logger.warning(f"Failed to delete source file: {e}")
122
+ except (RuntimeError, FileNotFoundError, OSError) as e:
123
+ logger.error(f"Conversion failed for {video}: {e}")
124
+ results.append((label, False, str(e)))
125
+ except KeyboardInterrupt:
126
+ console.print("\n[bold yellow]Conversion interrupted by user[/bold yellow]")
127
+
128
+ table = Table(title="Conversion Summary", title_style="bold cyan", border_style="cyan", show_header=False, padding=(0, 2))
129
+ table.add_column("File", style="cyan")
130
+ table.add_column("Result", style="white")
131
+ for label, success, detail in results:
132
+ if success:
133
+ table.add_row(label, f"[bold green]OK[/] [dim white]-> {detail}[/]")
134
+ else:
135
+ table.add_row(label, f"[bold red]FAILED:[/] {detail}")
136
+ console.print()
137
+
138
+ failed_count = total - converted_count
139
+ if converted_count == total:
140
+ console.print(f"[bold green]{converted_count} file(s) converted successfully.[/]")
141
+ elif converted_count > 0:
142
+ console.print(f"[bold yellow]{converted_count}/{total} file(s) converted ({failed_count} failed).[/]")
143
+ else:
144
+ console.print("[bold red]All conversions failed.[/]")
145
+ console.print()
146
+
147
+ return converted_count > 0