yt-aug 0.1.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.
- yt_aug-0.1.0/PKG-INFO +82 -0
- yt_aug-0.1.0/README.md +72 -0
- yt_aug-0.1.0/pyproject.toml +35 -0
- yt_aug-0.1.0/setup.cfg +4 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/PKG-INFO +82 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/SOURCES.txt +12 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/dependency_links.txt +1 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/entry_points.txt +2 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/requires.txt +3 -0
- yt_aug-0.1.0/src/yt_aug.egg-info/top_level.txt +1 -0
- yt_aug-0.1.0/src/ytaug/__init__.py +0 -0
- yt_aug-0.1.0/src/ytaug/download.py +177 -0
- yt_aug-0.1.0/src/ytaug/exceptions.py +6 -0
- yt_aug-0.1.0/src/ytaug/main.py +86 -0
yt_aug-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yt-aug
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: YouTube Music Manager CLI
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer>=0.25.0
|
|
8
|
+
Requires-Dist: yt-dlp>=2026.3.17
|
|
9
|
+
Requires-Dist: yt-dlp-ejs>=0.8.0
|
|
10
|
+
|
|
11
|
+
# yt-aug — YouTube Augment
|
|
12
|
+
|
|
13
|
+
Download audio from YouTube videos and playlists — from the command line.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install yt-aug
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.13+.
|
|
22
|
+
|
|
23
|
+
## System Requirements
|
|
24
|
+
|
|
25
|
+
yt-aug uses `yt-dlp` under the hood, which needs the following installed on your machine:
|
|
26
|
+
|
|
27
|
+
| Binary | Purpose | Windows | macOS | Linux |
|
|
28
|
+
|--------|---------|---------|-------|-------|
|
|
29
|
+
| `ffmpeg` | Audio extraction/conversion | `winget install ffmpeg` | `brew install ffmpeg` | `sudo apt install ffmpeg` |
|
|
30
|
+
| `deno` or `node` (≥ 20) | JS runtime for YouTube challenge solving | `winget install deno` | `brew install deno` | `curl -fsSL https://deno.land/install.sh \| sh` |
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### `download <url>`
|
|
35
|
+
|
|
36
|
+
Downloads audio from a YouTube video or playlist. Extracts best available audio and converts to m4a at 192kbps via FFmpeg.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ytaug download "https://youtube.com/watch?v=..." -o ~/Music
|
|
40
|
+
ytaug download "https://youtube.com/playlist?list=PL..." -o ~/Music
|
|
41
|
+
ytaug download "https://music.youtube.com/playlist?list=PL..."
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Before downloading, ytaug will:
|
|
45
|
+
1. Check for a JS runtime and ffmpeg
|
|
46
|
+
2. Fetch the URL metadata (title, type, track count for playlists)
|
|
47
|
+
3. Ask for confirmation
|
|
48
|
+
|
|
49
|
+
For playlists, files are organized into a subfolder named after the playlist.
|
|
50
|
+
|
|
51
|
+
| Flag | Short | Description |
|
|
52
|
+
|------|-------|-------------|
|
|
53
|
+
| `--output` | `-o` | Output directory (default: current directory) |
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Install with dev dependencies
|
|
59
|
+
uv sync
|
|
60
|
+
|
|
61
|
+
# Run tests
|
|
62
|
+
uv run pytest
|
|
63
|
+
|
|
64
|
+
# Run unit tests only
|
|
65
|
+
uv run pytest tests/unit
|
|
66
|
+
|
|
67
|
+
# Run integration tests only (requires JS runtime + ffmpeg + network)
|
|
68
|
+
uv run pytest tests/integration -m integration
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/ytaug/
|
|
75
|
+
├── main.py Typer CLI entry point (orchestration + user I/O)
|
|
76
|
+
├── download.py yt-dlp wrapper (system checks, URL validation, metadata, downloads)
|
|
77
|
+
└── exceptions.py YTAugError hierarchy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `main.py` handles all CLI interaction (prompts, messages, exits)
|
|
81
|
+
- `download.py` is pure logic with no console output
|
|
82
|
+
- All library functions raise `YTAugError` subclasses on operational failure
|
yt_aug-0.1.0/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# yt-aug — YouTube Augment
|
|
2
|
+
|
|
3
|
+
Download audio from YouTube videos and playlists — from the command line.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install yt-aug
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.13+.
|
|
12
|
+
|
|
13
|
+
## System Requirements
|
|
14
|
+
|
|
15
|
+
yt-aug uses `yt-dlp` under the hood, which needs the following installed on your machine:
|
|
16
|
+
|
|
17
|
+
| Binary | Purpose | Windows | macOS | Linux |
|
|
18
|
+
|--------|---------|---------|-------|-------|
|
|
19
|
+
| `ffmpeg` | Audio extraction/conversion | `winget install ffmpeg` | `brew install ffmpeg` | `sudo apt install ffmpeg` |
|
|
20
|
+
| `deno` or `node` (≥ 20) | JS runtime for YouTube challenge solving | `winget install deno` | `brew install deno` | `curl -fsSL https://deno.land/install.sh \| sh` |
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### `download <url>`
|
|
25
|
+
|
|
26
|
+
Downloads audio from a YouTube video or playlist. Extracts best available audio and converts to m4a at 192kbps via FFmpeg.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ytaug download "https://youtube.com/watch?v=..." -o ~/Music
|
|
30
|
+
ytaug download "https://youtube.com/playlist?list=PL..." -o ~/Music
|
|
31
|
+
ytaug download "https://music.youtube.com/playlist?list=PL..."
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Before downloading, ytaug will:
|
|
35
|
+
1. Check for a JS runtime and ffmpeg
|
|
36
|
+
2. Fetch the URL metadata (title, type, track count for playlists)
|
|
37
|
+
3. Ask for confirmation
|
|
38
|
+
|
|
39
|
+
For playlists, files are organized into a subfolder named after the playlist.
|
|
40
|
+
|
|
41
|
+
| Flag | Short | Description |
|
|
42
|
+
|------|-------|-------------|
|
|
43
|
+
| `--output` | `-o` | Output directory (default: current directory) |
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Install with dev dependencies
|
|
49
|
+
uv sync
|
|
50
|
+
|
|
51
|
+
# Run tests
|
|
52
|
+
uv run pytest
|
|
53
|
+
|
|
54
|
+
# Run unit tests only
|
|
55
|
+
uv run pytest tests/unit
|
|
56
|
+
|
|
57
|
+
# Run integration tests only (requires JS runtime + ffmpeg + network)
|
|
58
|
+
uv run pytest tests/integration -m integration
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
src/ytaug/
|
|
65
|
+
├── main.py Typer CLI entry point (orchestration + user I/O)
|
|
66
|
+
├── download.py yt-dlp wrapper (system checks, URL validation, metadata, downloads)
|
|
67
|
+
└── exceptions.py YTAugError hierarchy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `main.py` handles all CLI interaction (prompts, messages, exits)
|
|
71
|
+
- `download.py` is pure logic with no console output
|
|
72
|
+
- All library functions raise `YTAugError` subclasses on operational failure
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "yt-aug"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "YouTube Music Manager CLI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"typer>=0.25.0",
|
|
9
|
+
"yt-dlp>=2026.3.17",
|
|
10
|
+
"yt-dlp-ejs>=0.8.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.packages.find]
|
|
14
|
+
where = ["src"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=68.0"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"pytest-mock>=3.15.1",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
markers = [
|
|
29
|
+
"unit: Quick logic tests that require zero external tools.",
|
|
30
|
+
"integration: Real runtime environment or platform binary execution tests.",
|
|
31
|
+
"local_only: Tests that should only be run on local machine."
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
ytaug = "ytaug.main:app"
|
yt_aug-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yt-aug
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: YouTube Music Manager CLI
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer>=0.25.0
|
|
8
|
+
Requires-Dist: yt-dlp>=2026.3.17
|
|
9
|
+
Requires-Dist: yt-dlp-ejs>=0.8.0
|
|
10
|
+
|
|
11
|
+
# yt-aug — YouTube Augment
|
|
12
|
+
|
|
13
|
+
Download audio from YouTube videos and playlists — from the command line.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install yt-aug
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.13+.
|
|
22
|
+
|
|
23
|
+
## System Requirements
|
|
24
|
+
|
|
25
|
+
yt-aug uses `yt-dlp` under the hood, which needs the following installed on your machine:
|
|
26
|
+
|
|
27
|
+
| Binary | Purpose | Windows | macOS | Linux |
|
|
28
|
+
|--------|---------|---------|-------|-------|
|
|
29
|
+
| `ffmpeg` | Audio extraction/conversion | `winget install ffmpeg` | `brew install ffmpeg` | `sudo apt install ffmpeg` |
|
|
30
|
+
| `deno` or `node` (≥ 20) | JS runtime for YouTube challenge solving | `winget install deno` | `brew install deno` | `curl -fsSL https://deno.land/install.sh \| sh` |
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### `download <url>`
|
|
35
|
+
|
|
36
|
+
Downloads audio from a YouTube video or playlist. Extracts best available audio and converts to m4a at 192kbps via FFmpeg.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ytaug download "https://youtube.com/watch?v=..." -o ~/Music
|
|
40
|
+
ytaug download "https://youtube.com/playlist?list=PL..." -o ~/Music
|
|
41
|
+
ytaug download "https://music.youtube.com/playlist?list=PL..."
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Before downloading, ytaug will:
|
|
45
|
+
1. Check for a JS runtime and ffmpeg
|
|
46
|
+
2. Fetch the URL metadata (title, type, track count for playlists)
|
|
47
|
+
3. Ask for confirmation
|
|
48
|
+
|
|
49
|
+
For playlists, files are organized into a subfolder named after the playlist.
|
|
50
|
+
|
|
51
|
+
| Flag | Short | Description |
|
|
52
|
+
|------|-------|-------------|
|
|
53
|
+
| `--output` | `-o` | Output directory (default: current directory) |
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Install with dev dependencies
|
|
59
|
+
uv sync
|
|
60
|
+
|
|
61
|
+
# Run tests
|
|
62
|
+
uv run pytest
|
|
63
|
+
|
|
64
|
+
# Run unit tests only
|
|
65
|
+
uv run pytest tests/unit
|
|
66
|
+
|
|
67
|
+
# Run integration tests only (requires JS runtime + ffmpeg + network)
|
|
68
|
+
uv run pytest tests/integration -m integration
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/ytaug/
|
|
75
|
+
├── main.py Typer CLI entry point (orchestration + user I/O)
|
|
76
|
+
├── download.py yt-dlp wrapper (system checks, URL validation, metadata, downloads)
|
|
77
|
+
└── exceptions.py YTAugError hierarchy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `main.py` handles all CLI interaction (prompts, messages, exits)
|
|
81
|
+
- `download.py` is pure logic with no console output
|
|
82
|
+
- All library functions raise `YTAugError` subclasses on operational failure
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/yt_aug.egg-info/PKG-INFO
|
|
4
|
+
src/yt_aug.egg-info/SOURCES.txt
|
|
5
|
+
src/yt_aug.egg-info/dependency_links.txt
|
|
6
|
+
src/yt_aug.egg-info/entry_points.txt
|
|
7
|
+
src/yt_aug.egg-info/requires.txt
|
|
8
|
+
src/yt_aug.egg-info/top_level.txt
|
|
9
|
+
src/ytaug/__init__.py
|
|
10
|
+
src/ytaug/download.py
|
|
11
|
+
src/ytaug/exceptions.py
|
|
12
|
+
src/ytaug/main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ytaug
|
|
File without changes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import platform
|
|
3
|
+
import yt_dlp
|
|
4
|
+
import urllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ytaug.exceptions import YTAugError
|
|
7
|
+
|
|
8
|
+
# TODO: handle playlist does not exist:"https://www.youtube.com/playlist?list=PLwivhteH3vK_S0yV2w3gCh_zM-2S_3N-Z"
|
|
9
|
+
# TODO: handle no internet connection error
|
|
10
|
+
# TODO: handle private video/playlist error
|
|
11
|
+
# TODO: raise custom error for only expected errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def has_ffmpeg() -> bool:
|
|
15
|
+
return shutil.which("ffmpeg") is not None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_ffmpeg_install_instructions() -> str:
|
|
19
|
+
os_name = platform.system()
|
|
20
|
+
instructions = {
|
|
21
|
+
"Windows": "winget install ffmpeg \nor check their website: https://ffmpeg.org",
|
|
22
|
+
"Darwin": "brew install ffmpeg \nor check their website: https://ffmpeg.org",
|
|
23
|
+
"Linux": "sudo apt install ffmpeg (Debian/Ubuntu) \nor sudo dnf install ffmpeg (Fedora) \nor check their website: https://ffmpeg.org",
|
|
24
|
+
}
|
|
25
|
+
return instructions.get(
|
|
26
|
+
os_name, "check their website for download instructions \nhttps://ffmpeg.org"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def has_js_runtime() -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Checks for available JS runtimes in order of preference.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if at least one runtime is available, False otherwise.
|
|
36
|
+
"""
|
|
37
|
+
for runtime in ("deno", "node"):
|
|
38
|
+
full_path = shutil.which(runtime)
|
|
39
|
+
if full_path:
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_ytdlp_js_runtime_config() -> dict:
|
|
45
|
+
"""
|
|
46
|
+
Returns a yt-dlp js_runtimes config dict with all found JS runtimes.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
dict in js_runtimes format, e.g. {"deno": {"path": "/usr/bin/deno"}}.
|
|
50
|
+
Empty dict if no runtime found.
|
|
51
|
+
"""
|
|
52
|
+
config = {}
|
|
53
|
+
for runtime in ("deno", "node"):
|
|
54
|
+
path = shutil.which(runtime)
|
|
55
|
+
if path:
|
|
56
|
+
config[runtime] = {"path": path}
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_js_runtime_install_instructions() -> str:
|
|
61
|
+
os_name = platform.system()
|
|
62
|
+
instructions = {
|
|
63
|
+
"Windows": "winget install deno OR winget install OpenJS.NodeJS \nor check their website: \nhttps://deno.com OR https://nodejs.org",
|
|
64
|
+
"Darwin": "brew install deno OR brew install node \nor check their website: \nhttps://deno.com OR https://nodejs.org",
|
|
65
|
+
"Linux": "curl -fsSL https://deno.land/install.sh | sh OR See https://nodejs.org/en/download/package-manager \nor check their website: \nhttps://deno.com OR https://nodejs.org",
|
|
66
|
+
}
|
|
67
|
+
return instructions.get(
|
|
68
|
+
os_name,
|
|
69
|
+
"check their website for download instructions \nhttps://deno.com OR See https://nodejs.org",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_youtube_url(url: str | None) -> bool:
|
|
74
|
+
if not url:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
parsed = urllib.parse.urlparse(url)
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
domain = parsed.netloc or parsed.path.split("/")[0]
|
|
83
|
+
domain = domain.lower()
|
|
84
|
+
domain = domain.removeprefix("www.")
|
|
85
|
+
|
|
86
|
+
return domain in ("youtube.com", "m.youtube.com", "music.youtube.com", "youtu.be")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_url_info_ytdlp(url: str, js_runtime_config: dict) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
runtime: {"path": full_path}}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# dummy logger class to pass yt_dlp's logger so that it doesnt log anything to terminal
|
|
95
|
+
class _NullLogger:
|
|
96
|
+
def debug(self, msg):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def info(self, msg):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def warning(self, msg):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def error(self, msg):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
ydl_opts = {
|
|
109
|
+
"quiet": True,
|
|
110
|
+
"no_warnings": True,
|
|
111
|
+
"extract_flat": True,
|
|
112
|
+
"js_runtimes": js_runtime_config,
|
|
113
|
+
"logger": _NullLogger(),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
118
|
+
info = ydl.extract_info(url, download=False)
|
|
119
|
+
|
|
120
|
+
except yt_dlp.utils.DownloadError as e:
|
|
121
|
+
if "Error 400" in str(e):
|
|
122
|
+
raise YTAugError("Provided video/playlist URL does not exist on youtube")
|
|
123
|
+
|
|
124
|
+
# if its not Error 400:
|
|
125
|
+
raise Exception("Unexpected error in get_url_info_ytdlp") from e
|
|
126
|
+
|
|
127
|
+
url_info = {
|
|
128
|
+
"type": info.get("_type"),
|
|
129
|
+
"title": info.get("title"),
|
|
130
|
+
"id": info.get("id"),
|
|
131
|
+
}
|
|
132
|
+
if (url_info.get("type")) == "playlist":
|
|
133
|
+
url_info["video_count"] = info.get("playlist_count")
|
|
134
|
+
|
|
135
|
+
return url_info
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def download_url_ytdlp(
|
|
139
|
+
url: str, target_path: Path, is_playlist: bool, js_runtime_config: dict
|
|
140
|
+
) -> None:
|
|
141
|
+
ydl_opts = {
|
|
142
|
+
# "quiet": True,
|
|
143
|
+
"format": "bestaudio/best",
|
|
144
|
+
"outtmpl": str(target_path / "%(title)s.%(ext)s"),
|
|
145
|
+
"js_runtimes": js_runtime_config,
|
|
146
|
+
"postprocessors": [
|
|
147
|
+
{
|
|
148
|
+
"key": "FFmpegExtractAudio",
|
|
149
|
+
"preferredcodec": "m4a",
|
|
150
|
+
"preferredquality": "192",
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
if is_playlist:
|
|
155
|
+
ydl_opts["outtmpl"] = str(
|
|
156
|
+
target_path / "%(playlist_title)s" / "%(title)s.%(ext)s"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
161
|
+
ydl.download([url])
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise Exception("Unexpected error in download_url_ytdlp") from e
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
ydl_opts = {
|
|
168
|
+
"quiet": True,
|
|
169
|
+
"no_warnings": True,
|
|
170
|
+
"extract_flat": True,
|
|
171
|
+
"js_runtimes": get_ytdlp_js_runtime_config,
|
|
172
|
+
}
|
|
173
|
+
url = None
|
|
174
|
+
|
|
175
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
176
|
+
info = ydl.extract_info(url, download=False)
|
|
177
|
+
print(info)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import traceback
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ytaug.download import (
|
|
7
|
+
has_ffmpeg,
|
|
8
|
+
get_ffmpeg_install_instructions,
|
|
9
|
+
has_js_runtime,
|
|
10
|
+
get_js_runtime_install_instructions,
|
|
11
|
+
get_ytdlp_js_runtime_config,
|
|
12
|
+
get_url_info_ytdlp,
|
|
13
|
+
download_url_ytdlp,
|
|
14
|
+
is_youtube_url,
|
|
15
|
+
)
|
|
16
|
+
from ytaug.exceptions import YTAugError
|
|
17
|
+
|
|
18
|
+
# TODO: save unhandled exceptions in log
|
|
19
|
+
# TODO: print traceback of all errors except custom errors
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(no_args_is_help=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command(help="Download audio from a YouTube playlist as m4a (192kbps)")
|
|
25
|
+
def download(
|
|
26
|
+
url: Annotated[str, typer.Argument(help="YouTube video or playlist URL")],
|
|
27
|
+
output: Annotated[
|
|
28
|
+
Path,
|
|
29
|
+
typer.Option("--output", "-o", help="Output directory for downloaded files"),
|
|
30
|
+
] = Path.cwd(),
|
|
31
|
+
):
|
|
32
|
+
# check ffmpeg, js runtime and valid youtbe url
|
|
33
|
+
if not has_js_runtime():
|
|
34
|
+
typer.echo(
|
|
35
|
+
"A JavaScript runtime (deno or node) is required but not found on your system."
|
|
36
|
+
)
|
|
37
|
+
typer.echo(get_js_runtime_install_instructions())
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
if not has_ffmpeg():
|
|
41
|
+
typer.echo("ffmpeg is required but not found on your system.")
|
|
42
|
+
typer.echo(get_ffmpeg_install_instructions())
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
if not is_youtube_url(url):
|
|
46
|
+
typer.echo("Provided URL is not a valid youtube domain's URL")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# confirm info of video/playlist and download location
|
|
51
|
+
info = get_url_info_ytdlp(
|
|
52
|
+
url=url, js_runtime_config=get_ytdlp_js_runtime_config()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if info["type"] == "playlist":
|
|
56
|
+
dest = Path(output, info["title"])
|
|
57
|
+
confirm = typer.confirm(
|
|
58
|
+
f'Download playlist: "{info["title"]}" ({info["video_count"]} tracks) → {dest}'
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
confirm = typer.confirm(f'Download video: "{info["title"]}" → {output}')
|
|
62
|
+
if not confirm:
|
|
63
|
+
typer.echo("Download cancelled.")
|
|
64
|
+
raise typer.Exit(0)
|
|
65
|
+
|
|
66
|
+
# download the files
|
|
67
|
+
|
|
68
|
+
download_url_ytdlp(
|
|
69
|
+
url=url,
|
|
70
|
+
target_path=output,
|
|
71
|
+
is_playlist=info["type"] == "playlist",
|
|
72
|
+
js_runtime_config=get_ytdlp_js_runtime_config(),
|
|
73
|
+
)
|
|
74
|
+
typer.echo("Download complete.")
|
|
75
|
+
|
|
76
|
+
except typer.Exit:
|
|
77
|
+
raise
|
|
78
|
+
except YTAugError as e:
|
|
79
|
+
typer.echo(f"App error: {e}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
typer.echo(traceback.print_exception(e))
|
|
82
|
+
typer.echo(f"Uncaught Exception: {e}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
app()
|