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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ ytaug = ytaug.main:app
@@ -0,0 +1,3 @@
1
+ typer>=0.25.0
2
+ yt-dlp>=2026.3.17
3
+ yt-dlp-ejs>=0.8.0
@@ -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,6 @@
1
+ class YTAugError(Exception):
2
+ """Base exception for all ytaug errors."""
3
+
4
+
5
+ class SystemRequirementError(YTAugError):
6
+ """Missing system dependencies (ffmpeg, JS runtime)."""
@@ -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()