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.
- idm_cli-1.4.0/MANIFEST.in +1 -0
- {idm_cli-1.3.0/idm_cli.egg-info → idm_cli-1.4.0}/PKG-INFO +28 -88
- {idm_cli-1.3.0 → idm_cli-1.4.0}/README.md +21 -78
- idm_cli-1.4.0/idm_cli/__init__.py +3 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/config.py +17 -7
- idm_cli-1.4.0/idm_cli/downloader/converter.py +147 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/core.py +33 -93
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/downloader.py +60 -23
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/handlers.py +58 -15
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/muxer.py +244 -179
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/state.py +5 -3
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/__init__.py +18 -18
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/winget.py +5 -1
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/ui/cli.py +30 -19
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/ui/utils.py +12 -4
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/update_checker.py +5 -2
- {idm_cli-1.3.0 → idm_cli-1.4.0/idm_cli.egg-info}/PKG-INFO +28 -88
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/SOURCES.txt +5 -5
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/requires.txt +3 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/top_level.txt +1 -0
- idm_cli-1.4.0/pyproject.toml +45 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/requirements.txt +0 -2
- {idm_cli-1.3.0 → idm_cli-1.4.0}/setup.py +1 -3
- idm_cli-1.4.0/tests/test_core.py +148 -0
- idm_cli-1.3.0/MANIFEST.in +0 -2
- idm_cli-1.3.0/idm_cli/__init__.py +0 -3
- idm_cli-1.3.0/idm_cli/extension/daemon.py +0 -44
- idm_cli-1.3.0/idm_cli/extension/extension.py +0 -161
- idm_cli-1.3.0/idm_cli/extension/native_host.py +0 -124
- {idm_cli-1.3.0 → idm_cli-1.4.0}/LICENSE +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/__main__.py +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/downloader/__init__.py +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/direct.py +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli/extractors/ytdlp.py +0 -0
- {idm_cli-1.3.0/idm_cli/extension → idm_cli-1.4.0/idm_cli/ui}/__init__.py +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/dependency_links.txt +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/idm_cli.egg-info/entry_points.txt +0 -0
- {idm_cli-1.3.0 → idm_cli-1.4.0}/setup.cfg +0 -0
- {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
|
+
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
|
-
|
|
26
|
-
Requires-Dist: pytest
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
+
<div align="center">
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
<h1>IDM-CLI</h1>
|
|
40
37
|
|
|
41
|
-
A lightning-fast, cross-platform command-line download manager written in Python
|
|
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
|
+
[](https://pypi.org/project/idm-cli/)
|
|
43
|
+
[](https://pepy.tech/project/idm-cli)
|
|
44
|
+
[](https://github.com/rj41-w2/idm-cli/blob/main/LICENSE)
|
|
45
|
+

|
|
46
|
+
[](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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1>IDM-CLI</h1>
|
|
4
4
|
|
|
5
|
-
A lightning-fast, cross-platform command-line download manager written in Python
|
|
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
|
+
[](https://pypi.org/project/idm-cli/)
|
|
10
|
+
[](https://pepy.tech/project/idm-cli)
|
|
11
|
+
[](https://github.com/rj41-w2/idm-cli/blob/main/LICENSE)
|
|
12
|
+

|
|
13
|
+
[](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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
46
|
-
|
|
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
|