idm-cli 1.3.1__tar.gz → 1.3.3__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 (48) hide show
  1. {idm_cli-1.3.1/idm_cli.egg-info → idm_cli-1.3.3}/PKG-INFO +5 -8
  2. {idm_cli-1.3.1 → idm_cli-1.3.3}/README.md +137 -137
  3. idm_cli-1.3.3/idm_cli/__init__.py +3 -0
  4. idm_cli-1.3.3/idm_cli/config.py +156 -0
  5. idm_cli-1.3.3/idm_cli/downloader/core.py +365 -0
  6. idm_cli-1.3.3/idm_cli/downloader/downloader.py +445 -0
  7. idm_cli-1.3.3/idm_cli/downloader/handlers.py +230 -0
  8. idm_cli-1.3.3/idm_cli/downloader/muxer.py +254 -0
  9. idm_cli-1.3.3/idm_cli/downloader/state.py +125 -0
  10. idm_cli-1.3.3/idm_cli/extractors/__init__.py +23 -0
  11. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/extractors/direct.py +14 -3
  12. idm_cli-1.3.3/idm_cli/extractors/winget.py +107 -0
  13. idm_cli-1.3.3/idm_cli/extractors/ytdlp.py +150 -0
  14. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/ui/cli.py +125 -54
  15. idm_cli-1.3.3/idm_cli/ui/progress.py +239 -0
  16. idm_cli-1.3.3/idm_cli/ui/prompts.py +322 -0
  17. idm_cli-1.3.3/idm_cli/ui/utils.py +190 -0
  18. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/update_checker.py +17 -9
  19. {idm_cli-1.3.1 → idm_cli-1.3.3/idm_cli.egg-info}/PKG-INFO +5 -8
  20. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli.egg-info/SOURCES.txt +4 -2
  21. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli.egg-info/requires.txt +3 -3
  22. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli.egg-info/top_level.txt +0 -1
  23. {idm_cli-1.3.1 → idm_cli-1.3.3}/pyproject.toml +41 -38
  24. {idm_cli-1.3.1 → idm_cli-1.3.3}/requirements.txt +9 -11
  25. idm_cli-1.3.3/tests/test_core.py +129 -0
  26. idm_cli-1.3.3/tests/test_regressions.py +76 -0
  27. idm_cli-1.3.1/idm_cli/__init__.py +0 -3
  28. idm_cli-1.3.1/idm_cli/config.py +0 -85
  29. idm_cli-1.3.1/idm_cli/downloader/core.py +0 -317
  30. idm_cli-1.3.1/idm_cli/downloader/downloader.py +0 -293
  31. idm_cli-1.3.1/idm_cli/downloader/handlers.py +0 -195
  32. idm_cli-1.3.1/idm_cli/downloader/muxer.py +0 -174
  33. idm_cli-1.3.1/idm_cli/downloader/state.py +0 -62
  34. idm_cli-1.3.1/idm_cli/extractors/__init__.py +0 -18
  35. idm_cli-1.3.1/idm_cli/extractors/winget.py +0 -76
  36. idm_cli-1.3.1/idm_cli/extractors/ytdlp.py +0 -139
  37. idm_cli-1.3.1/idm_cli/ui/utils.py +0 -153
  38. idm_cli-1.3.1/setup.py +0 -44
  39. idm_cli-1.3.1/tests/test_core.py +0 -76
  40. {idm_cli-1.3.1 → idm_cli-1.3.3}/LICENSE +0 -0
  41. {idm_cli-1.3.1 → idm_cli-1.3.3}/MANIFEST.in +0 -0
  42. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/__main__.py +0 -0
  43. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/downloader/__init__.py +0 -0
  44. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli/ui/__init__.py +0 -0
  45. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli.egg-info/dependency_links.txt +0 -0
  46. {idm_cli-1.3.1 → idm_cli-1.3.3}/idm_cli.egg-info/entry_points.txt +0 -0
  47. {idm_cli-1.3.1 → idm_cli-1.3.3}/setup.cfg +0 -0
  48. {idm_cli-1.3.1 → idm_cli-1.3.3}/tests/__init__.py +0 -0
@@ -1,8 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idm-cli
3
- Version: 1.3.1
3
+ Version: 1.3.3
4
4
  Summary: A lightning-fast, universal command-line download manager
5
- Home-page: https://github.com/rj41-w2/idm-cli
6
5
  Author: Rehan
7
6
  License-Expression: MIT
8
7
  Project-URL: Homepage, https://github.com/rj41-w2/idm-cli
@@ -14,22 +13,20 @@ Description-Content-Type: text/markdown
14
13
  License-File: LICENSE
15
14
  Requires-Dist: typer
16
15
  Requires-Dist: rich
17
- Requires-Dist: aiohttp
16
+ Requires-Dist: curl-cffi>=0.7.0
18
17
  Requires-Dist: aiofiles
19
18
  Requires-Dist: yt-dlp
20
19
  Requires-Dist: pyfiglet
21
20
  Requires-Dist: questionary
22
- Requires-Dist: aiodns
23
- Requires-Dist: Brotli
24
21
  Requires-Dist: psutil
25
22
  Requires-Dist: platformdirs
26
23
  Provides-Extra: dev
27
24
  Requires-Dist: pytest; extra == "dev"
28
25
  Requires-Dist: pytest-asyncio; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
29
27
  Requires-Dist: packaging; extra == "dev"
30
- Dynamic: home-page
28
+ Requires-Dist: ruff; extra == "dev"
31
29
  Dynamic: license-file
32
- Dynamic: requires-python
33
30
 
34
31
  <div align="center">
35
32
 
@@ -53,7 +50,7 @@ IDM-CLI splits files into multiple parallel chunks (default 8, up to 32) to maxi
53
50
 
54
51
  ## Features
55
52
 
56
- - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `aiohttp` async sessions to fully saturate your bandwidth.
53
+ - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `curl-cffi` async sessions to fully saturate your bandwidth.
57
54
  - **Universal Downloader** — Paste any URL. yt-dlp handles 1000+ sites (YouTube, Facebook, Instagram, etc.), with automatic fallback to direct HTTP download for unsupported URLs.
58
55
  - **Audio Only / MP3 Conversion** — Download videos as audio. IDM-CLI automatically grabs the best audio stream and uses FFmpeg to convert it to `.mp3`.
59
56
  - **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.
@@ -1,137 +1,137 @@
1
- <div align="center">
2
-
3
- <h1>IDM-CLI</h1>
4
-
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.).
20
-
21
- ## Features
22
-
23
- - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `aiohttp` async sessions to fully saturate your bandwidth.
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.
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`.
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.
27
- - **Persistent Queue System** — Add multiple files to a queue with `-Q` and type `start queue` to download them all sequentially.
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.
29
- - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling.
30
- - **Auto-Update Check** — Checks PyPI once a day and notifies you when a new version is available.
31
-
32
- ## Prerequisites
33
-
34
- - **Python 3.8+**
35
- - **FFmpeg** — Required for muxing video and audio streams, and converting to `.mp3`. IDM-CLI can auto-download it for you, or you can install it manually:
36
- - Windows: `winget install ffmpeg`
37
- - macOS: `brew install ffmpeg`
38
- - Linux: `sudo apt install ffmpeg`
39
-
40
- ## Installation
41
-
42
- ```bash
43
- # From PyPI
44
- pip install idm-cli
45
-
46
- # From source
47
- git clone https://github.com/rj41-w2/idm-cli.git
48
- cd IDM-CLI
49
- pip install -r requirements.txt
50
- pip install -e .
51
- ```
52
-
53
- ## Usage
54
-
55
- Run the app interactively:
56
-
57
- ```bash
58
- idm
59
- ```
60
-
61
- This opens a prompt where you can paste your link, select video quality, or queue the download.
62
-
63
- ### CLI Flags (Fast Mode)
64
-
65
- Skip the interactive menus by passing arguments directly:
66
-
67
- ```bash
68
- # Download a video at 1080p
69
- idm "https://youtube.com/watch?v=..." -q 1080p -v
70
-
71
- # Audio only (converts to MP3)
72
- idm "https://youtube.com/watch?v=..." -a
73
-
74
- # 16 parallel chunks for maximum speed
75
- idm "https://example.com/largefile.zip" -c 16
76
-
77
- # Add to queue
78
- idm "https://facebook.com/..." -Q
79
- ```
80
-
81
- | Flag | Description |
82
- |------|-------------|
83
- | `-q <resolution>` | Set video quality (e.g., `1080p`, `720p`) |
84
- | `-a` | Audio only — download and convert to MP3 |
85
- | `-v` | Download video + audio (bypasses quality prompt) |
86
- | `-Q` | Add to queue instead of downloading immediately |
87
- | `-c <num>` | Number of parallel chunks (default: 8) |
88
- | `-f <filename>` | Force a custom output filename |
89
-
90
- ### Interactive Commands
91
-
92
- | Command | Description |
93
- |---------|-------------|
94
- | `<URL>` | Download a video or file |
95
- | `resume` | Resume or delete incomplete downloads |
96
- | `start queue` | Process all queued downloads |
97
- | `help` | Show available commands and flags |
98
- | `exit` | Exit the application |
99
-
100
- ### Troubleshooting: Windows Device Guard
101
-
102
- If you get `"idm.exe was blocked by your organization's Device Guard policy"`, run the Python module directly:
103
-
104
- ```bash
105
- python -m idm_cli
106
- ```
107
-
108
- ## Architecture
109
-
110
- ```
111
- idm_cli/
112
- ├── config.py # Platform-aware config, logging, paths
113
- ├── update_checker.py # PyPI version check
114
- ├── ui/
115
- │ ├── cli.py # Main interactive CLI
116
- │ └── utils.py # Console, progress listener, validators
117
- ├── downloader/
118
- │ ├── core.py # Download orchestration & FFmpeg setup
119
- │ ├── downloader.py # Async chunk download engine
120
- │ ├── handlers.py # Queue & resume handlers
121
- │ ├── muxer.py # FFmpeg mux/conversion
122
- │ └── state.py # Atomic JSON state persistence
123
- ├── extractors/
124
- │ ├── ytdlp.py # YouTube/social media extractor
125
- │ ├── direct.py # Direct HTTP file downloader
126
- │ └── winget.py # Windows Package Manager extractor
127
- ```
128
-
129
- ## Contributing
130
-
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.
132
-
133
- For bug reports and feature requests, [open an issue](https://github.com/rj41-w2/idm-cli/issues).
134
-
135
- ## License
136
-
137
- This project is licensed under the [MIT License](LICENSE).
1
+ <div align="center">
2
+
3
+ <h1>IDM-CLI</h1>
4
+
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.).
20
+
21
+ ## Features
22
+
23
+ - **Parallel Chunk Engine** — Splits downloads into multiple concurrent chunks using `curl-cffi` async sessions to fully saturate your bandwidth.
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.
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`.
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.
27
+ - **Persistent Queue System** — Add multiple files to a queue with `-Q` and type `start queue` to download them all sequentially.
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.
29
+ - **Cross-Platform** — Works on Windows, macOS, and Linux with platform-specific FFmpeg handling.
30
+ - **Auto-Update Check** — Checks PyPI once a day and notifies you when a new version is available.
31
+
32
+ ## Prerequisites
33
+
34
+ - **Python 3.8+**
35
+ - **FFmpeg** — Required for muxing video and audio streams, and converting to `.mp3`. IDM-CLI can auto-download it for you, or you can install it manually:
36
+ - Windows: `winget install ffmpeg`
37
+ - macOS: `brew install ffmpeg`
38
+ - Linux: `sudo apt install ffmpeg`
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ # From PyPI
44
+ pip install idm-cli
45
+
46
+ # From source
47
+ git clone https://github.com/rj41-w2/idm-cli.git
48
+ cd IDM-CLI
49
+ pip install -r requirements.txt
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ Run the app interactively:
56
+
57
+ ```bash
58
+ idm
59
+ ```
60
+
61
+ This opens a prompt where you can paste your link, select video quality, or queue the download.
62
+
63
+ ### CLI Flags (Fast Mode)
64
+
65
+ Skip the interactive menus by passing arguments directly:
66
+
67
+ ```bash
68
+ # Download a video at 1080p
69
+ idm "https://youtube.com/watch?v=..." -q 1080p -v
70
+
71
+ # Audio only (converts to MP3)
72
+ idm "https://youtube.com/watch?v=..." -a
73
+
74
+ # 16 parallel chunks for maximum speed
75
+ idm "https://example.com/largefile.zip" -c 16
76
+
77
+ # Add to queue
78
+ idm "https://facebook.com/..." -Q
79
+ ```
80
+
81
+ | Flag | Description |
82
+ |------|-------------|
83
+ | `-q <resolution>` | Set video quality (e.g., `1080p`, `720p`) |
84
+ | `-a` | Audio only — download and convert to MP3 |
85
+ | `-v` | Download video + audio (bypasses quality prompt) |
86
+ | `-Q` | Add to queue instead of downloading immediately |
87
+ | `-c <num>` | Number of parallel chunks (default: 8) |
88
+ | `-f <filename>` | Force a custom output filename |
89
+
90
+ ### Interactive Commands
91
+
92
+ | Command | Description |
93
+ |---------|-------------|
94
+ | `<URL>` | Download a video or file |
95
+ | `resume` | Resume or delete incomplete downloads |
96
+ | `start queue` | Process all queued downloads |
97
+ | `help` | Show available commands and flags |
98
+ | `exit` | Exit the application |
99
+
100
+ ### Troubleshooting: Windows Device Guard
101
+
102
+ If you get `"idm.exe was blocked by your organization's Device Guard policy"`, run the Python module directly:
103
+
104
+ ```bash
105
+ python -m idm_cli
106
+ ```
107
+
108
+ ## Architecture
109
+
110
+ ```
111
+ idm_cli/
112
+ ├── config.py # Platform-aware config, logging, paths
113
+ ├── update_checker.py # PyPI version check
114
+ ├── ui/
115
+ │ ├── cli.py # Main interactive CLI
116
+ │ └── utils.py # Console, progress listener, validators
117
+ ├── downloader/
118
+ │ ├── core.py # Download orchestration & FFmpeg setup
119
+ │ ├── downloader.py # Async chunk download engine
120
+ │ ├── handlers.py # Queue & resume handlers
121
+ │ ├── muxer.py # FFmpeg mux/conversion
122
+ │ └── state.py # Atomic JSON state persistence
123
+ ├── extractors/
124
+ │ ├── ytdlp.py # YouTube/social media extractor
125
+ │ ├── direct.py # Direct HTTP file downloader
126
+ │ └── winget.py # Windows Package Manager extractor
127
+ ```
128
+
129
+ ## Contributing
130
+
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.
132
+
133
+ For bug reports and feature requests, [open an issue](https://github.com/rj41-w2/idm-cli/issues).
134
+
135
+ ## License
136
+
137
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,3 @@
1
+ # IDM-CLI Package
2
+
3
+ __version__ = "1.3.3"
@@ -0,0 +1,156 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import shutil
5
+ from logging.handlers import RotatingFileHandler
6
+
7
+ from platformdirs import user_data_dir, user_downloads_dir
8
+
9
+ __all__ = [
10
+ "CONFIG_DIR",
11
+ "CONFIG_FILE",
12
+ "load_config",
13
+ "logger",
14
+ "mute_console_logging",
15
+ "save_config",
16
+ "setup_logging",
17
+ "unmute_console_logging",
18
+ ]
19
+
20
+ APP_NAME = "idm-cli"
21
+ APP_AUTHOR = "idm-cli"
22
+ CONFIG_DIR = user_data_dir(APP_NAME, APP_AUTHOR)
23
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
24
+ LOG_FILE = os.path.join(CONFIG_DIR, "idm.log")
25
+
26
+
27
+ def _migrate_old_config() -> None:
28
+ old_dir = os.path.expanduser("~/.idm_cli")
29
+ if old_dir == CONFIG_DIR:
30
+ return
31
+ if not os.path.isdir(old_dir):
32
+ return
33
+ os.makedirs(CONFIG_DIR, exist_ok=True)
34
+ for item in os.listdir(old_dir):
35
+ if item == "__pycache__":
36
+ continue
37
+ src = os.path.join(old_dir, item)
38
+ dst = os.path.join(CONFIG_DIR, item)
39
+ if not os.path.exists(dst):
40
+ try:
41
+ if os.path.isdir(src):
42
+ shutil.copytree(src, dst)
43
+ else:
44
+ shutil.copy2(src, dst)
45
+ except OSError:
46
+ pass
47
+
48
+
49
+ def _get_default_download_dir() -> str:
50
+ return user_downloads_dir()
51
+
52
+
53
+ def setup_logging() -> logging.Logger:
54
+ logger = logging.getLogger("idm_cli")
55
+ logger.setLevel(logging.DEBUG)
56
+ if not logger.handlers:
57
+ formatter = logging.Formatter(
58
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
59
+ )
60
+
61
+ try:
62
+ _migrate_old_config()
63
+ os.makedirs(CONFIG_DIR, exist_ok=True)
64
+ file_handler = RotatingFileHandler(
65
+ LOG_FILE, maxBytes=5 * 1024 * 1024, backupCount=2, encoding="utf-8"
66
+ )
67
+ file_handler.setFormatter(formatter)
68
+ logger.addHandler(file_handler)
69
+ except OSError:
70
+ # Logging must never prevent the CLI from starting (for example in a
71
+ # read-only home directory or a restricted CI environment).
72
+ pass
73
+
74
+ stream_handler = logging.StreamHandler()
75
+ stream_handler.setLevel(logging.INFO)
76
+ stream_handler.setFormatter(formatter)
77
+ try:
78
+ stream_handler.stream.reconfigure(errors="backslashreplace")
79
+ except AttributeError:
80
+ pass
81
+ logger.addHandler(stream_handler)
82
+ return logger
83
+
84
+
85
+ logger = setup_logging()
86
+
87
+ DEFAULT_CONFIG = {
88
+ "default_chunks": 8,
89
+ "default_quality": "720p",
90
+ "download_dir": _get_default_download_dir(),
91
+ "last_version": "0.0.0",
92
+ }
93
+
94
+
95
+ def load_config() -> dict:
96
+ config = DEFAULT_CONFIG.copy()
97
+ if os.path.exists(CONFIG_FILE):
98
+ try:
99
+ with open(CONFIG_FILE, "r") as f:
100
+ user_config = json.load(f)
101
+ if isinstance(user_config, dict):
102
+ config.update(user_config)
103
+ else:
104
+ raise TypeError("config root must be an object")
105
+ except (OSError, TypeError, json.JSONDecodeError) as e:
106
+ logger.warning(f"Failed to load config: {e}")
107
+ chunks = config.get("default_chunks")
108
+ if not isinstance(chunks, int) or not 1 <= chunks <= 32:
109
+ config["default_chunks"] = DEFAULT_CONFIG["default_chunks"]
110
+ if not isinstance(config.get("default_quality"), str):
111
+ config["default_quality"] = DEFAULT_CONFIG["default_quality"]
112
+ if (
113
+ not isinstance(config.get("download_dir"), str)
114
+ or not config["download_dir"].strip()
115
+ ):
116
+ config["download_dir"] = DEFAULT_CONFIG["download_dir"]
117
+ return config
118
+
119
+
120
+ def save_config(config_data: dict) -> None:
121
+ os.makedirs(CONFIG_DIR, exist_ok=True)
122
+ try:
123
+ with open(CONFIG_FILE, "w") as f:
124
+ json.dump(config_data, f, indent=4)
125
+ except OSError as e:
126
+ logger.error(f"Failed to save config: {e}")
127
+
128
+
129
+ def mute_console_logging() -> None:
130
+ """Remove the StreamHandler from the logger so logs go to file only (used during active downloads)."""
131
+ _logger = logging.getLogger("idm_cli")
132
+ for handler in list(_logger.handlers):
133
+ if isinstance(handler, logging.StreamHandler) and not isinstance(
134
+ handler, RotatingFileHandler
135
+ ):
136
+ _logger.removeHandler(handler)
137
+
138
+
139
+ def unmute_console_logging() -> None:
140
+ """Re-attach a StreamHandler if it was removed."""
141
+ _logger = logging.getLogger("idm_cli")
142
+ has_stream = any(
143
+ isinstance(h, logging.StreamHandler) and not isinstance(h, RotatingFileHandler)
144
+ for h in _logger.handlers
145
+ )
146
+ if not has_stream:
147
+ formatter = logging.Formatter(
148
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
149
+ )
150
+ stream_handler = logging.StreamHandler()
151
+ stream_handler.setFormatter(formatter)
152
+ try:
153
+ stream_handler.stream.reconfigure(errors="backslashreplace")
154
+ except AttributeError:
155
+ pass
156
+ _logger.addHandler(stream_handler)